├── .gitattributes
├── README.md
└── support
├── Makefile
├── README.md.tpl
├── amber.png
├── diffs
└── shards.txt
├── haproxy.conf
├── ln-sfs
├── run-diffs.bash
├── shards.txt
└── tpl2md.pl
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.bash linguist-language=Crystal
2 | *.pl linguist-language=Crystal
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Introduction to the Amber Web Framework
4 | And its Out-of-the-Box Features
5 |
6 |
7 |
8 | Amber makes building web applications easy, fast, and enjoyable.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | # Table of Contents
17 |
18 | 1. [Introduction](#introduction)
19 | 1. [Installation](#installation)
20 | 1. [Creating New Amber App](#creating_new_amber_app)
21 | 1. [Running the App](#running_the_app)
22 | 1. [Building the App and Build Troubleshooting](#building_the_app_and_build_troubleshooting)
23 | 1. [REPL](#repl)
24 | 1. [File Structure](#file_structure)
25 | 1. [Database Commands](#database_commands)
26 | 1. [Pipes and Pipelines](#pipes_and_pipelines)
27 | 1. [Routes, Controller Methods, and Responses](#routes__controller_methods__and_responses)
28 | 1. [Views](#views)
29 | 1. [Template Languages](#template_languages)
30 | 1. [Liquid Template Language](#liquid_template_language)
31 | 1. [Logging](#logging)
32 | 1. [Parameter Validation](#parameter_validation)
33 | 1. [Static Pages](#static_pages)
34 | 1. [Variables in Views](#variables_in_views)
35 | 1. [More on Database Commands](#more_on_database_commands)
36 | 1. [Micrate](#micrate)
37 | 1. [Custom Migrations Engine](#custom_migrations_engine)
38 | 1. [Internationalization (I18n)](#internationalization__i18n_)
39 | 1. [Responses](#responses)
40 | 1. [Responses with Different Content-Type](#responses_with_different_content_type)
41 | 1. [Error Responses](#error_responses)
42 | 1. [Manual Error Responses](#manual_error_responses)
43 | 1. [Error Responses via Error Pipe](#error_responses_via_error_pipe)
44 | 1. [Assets Pipeline](#assets_pipeline)
45 | 1. [Adding jQuery and jQuery UI](#adding_jquery_and_jquery_ui)
46 | 1. [Resource Aliases](#resource_aliases)
47 | 1. [CSS Optimization / Minification](#css_optimization___minification)
48 | 1. [File Copying](#file_copying)
49 | 1. [Asset Management Alternatives](#asset_management_alternatives)
50 | 1. [Advanced Topics](#advanced_topics)
51 | 1. [Amber::Controller::Base](#amber__controller__base)
52 | 1. [Extensions](#extensions)
53 | 1. [Shards](#shards)
54 | 1. [Environments](#environments)
55 | 1. [Starting the Server](#starting_the_server)
56 | 1. [Serving Requests](#serving_requests)
57 | 1. [Support Routines](#support_routines)
58 | 1. [Amber behind a Load Balancer | Reverse Proxy | ADC](#amber_behind_a_load_balancer___reverse_proxy___adc)
59 |
60 |
61 | # Introduction
62 |
63 | **Amber** is a web application framework written in [Crystal](http://www.crystal-lang.org). Homepage can be found at [amberframework.org](https://amberframework.org/), docs at [Amber Docs](https://docs.amberframework.org), GitHub repository at [amberframework/amber](https://github.com/amberframework/amber), and the chat on [Gitter](https://gitter.im/amberframework/amber) or on the FreeNode IRC channel #amber.
64 |
65 | Amber is inspired by Kemal, Rails, Phoenix, and other frameworks. It is simple to get used to, and much more intuitive than frameworks like Rails. (But it does inherit many concepts from Rails that are good.)
66 |
67 | This document is here to describe everything that Amber offers out of the box, sorted in a logical order and easy to consult repeatedly over time. The Crystal level is not described; it is expected that the readers coming here have a formed understanding of [Crystal and its features](https://crystal-lang.org/docs/overview/).
68 |
69 | # Installation
70 |
71 | ```shell
72 | git clone https://github.com/amberframework/amber
73 | cd amber
74 | make # The result of 'make' will be one file -- command line tool bin/amber
75 |
76 | # To install the file, or to symlink the system-wide executable to current directory, run one of:
77 | make install # default PREFIX is /usr/local
78 | make install PREFIX=/usr/local/stow/amber
79 | make force_link # can also specify PREFIX=...
80 | ```
81 |
82 | ("stow" mentioned above is referring to [GNU Stow](https://www.gnu.org/software/stow/).)
83 |
84 | After installation or linking, `amber` is the command you will be using for creating and managing Amber apps.
85 |
86 | Please note that some users prefer (or must use for compatibility reasons) local Amber executables which match the version of Amber used in their project. For that, each Amber project's `shard.yml` ships with the build target named "amber":
87 |
88 | ```
89 | targets:
90 | ...
91 | amber:
92 | main: lib/amber/src/amber/cli.cr
93 |
94 | ```
95 |
96 | Thanks to it, running `shards build amber` will compile local Amber found in `lib/amber/` and place the executable into the project's local file `bin/amber`.
97 |
98 | # Creating New Amber App
99 |
100 | ```shell
101 | amber new [-d DATABASE] [-t TEMPLATE_LANG] [-m ORM_MODEL] [--deps]
102 | ```
103 |
104 | Supported databases are [PostgreSQL](https://www.postgresql.org/) (pg, default), [MySQL](https://www.mysql.com/) (mysql), and [SQLite](https://sqlite.org/) (sqlite).
105 |
106 | Supported template languages are [slang](https://github.com/jeromegn/slang) (default) and [ecr](https://crystal-lang.org/api/0.21.1/ECR.html). (But any languages can be used; more on that can be found below in [Template Languages](#template_languages).)
107 |
108 | Slang is extremely elegant, but very different from the traditional perception of HTML.
109 | ECR is HTML-like, very similar to Ruby ERB, and also much less efficient than slang, but it may be the best choice for your application if you intend to use some HTML site template (e.g. from [themeforest](https://themeforest.net/)) whose pages are in HTML + CSS or SCSS. (Or you could also try [html2slang](https://github.com/docelic/html2slang/) which converts the bulk of HTML pages into slang with relatively good accuracy.)
110 |
111 | Supported ORM models are [granite](https://github.com/amberframework/granite-orm) (default) and [crecto](https://github.com/Crecto/crecto).
112 |
113 | Granite is Amber's native, nice, and effective ORM model where you mostly write your own SQL. For example, all search queries typically look like `YourModel.all("WHERE field1 = ? AND field2 = ?", [value1, value2])`. But it also has belongs/has relations, and some other little things.
114 |
115 | Supported migrations engine is [micrate](https://github.com/amberframework/micrate). (But any migrations engines can be used; more on that can be found below in [Custom Migrations Engine](#custom_migrations_engine).)
116 |
117 | Micrate is very simple and you basically write raw SQL in your migrations. There are just two keywords in the migration files which give instructions whether the SQLs that follow pertain to migrating up or down. These keywords are "-- +micrate Up" and "-- +micrate Down". If you have complex SQL statements that contain semicolons then you also enclose each in "-- +micrate StatementBegin" and "-- +micrate StatementEnd".
118 |
119 | Finally, if argument `--deps` is provided, Amber will automatically run `shards` in the new project's directory after creation to download the shards required by the project.
120 |
121 | Please note that shards-related commands use the directory `.shards/` as local staging area before the contents are fully ready to replace shards in `lib/`.
122 |
123 | # Running the App
124 |
125 | Before building or running Amber applications, you should install the following system packages: `libevent-dev libgc-dev libxml2-dev libssl-dev libyaml-dev libcrypto++-dev libsqlite3-dev`. These packages will make sure that you do not run into missing header files as soon as you try to run the application.
126 |
127 | Other than that, the app can be started as soon as you have created it and ran `shards` in the app directory.
128 | (It is not necessary to run `shards` if you have invoked `amber new` with the argument `--deps`; in that case Amber did it for you.)
129 |
130 | Please note that the application is always compiled, regardless of whether one is using the Crystal command 'run' (the default) or 'build'. It is just that in run mode, the resulting binary is typically compiled without optimizations (to improve build speed) and is not saved to a file, but is compiled, executed, and then discarded.
131 |
132 | To run the app, you could use a couple different approaches:
133 |
134 | ```shell
135 | # For development, clean and simple - compiles and runs your app:
136 | crystal src/.cr
137 |
138 | # Compiles and runs app in 'production' environment:
139 | AMBER_ENV=production crystal src/.cr
140 |
141 | # For development, clean and simple - compiles and runs your app, but
142 | # also watches for changes in files and rebuilds/re-runs automatically:
143 | amber watch
144 | ```
145 |
146 | Amber apps by default use a feature called "port reuse" available in newer Linux kernels. If you get an error "setsockopt: Protocol not available" upon running the app, it means your kernel does not support it. Please edit `config/environments/development.yml` and set "port_reuse" to false.
147 |
148 | # Building the App and Build Troubleshooting
149 |
150 | To build the application in a simple and effective way, you would run the following to produce executable file `bin/`:
151 |
152 | ```shell
153 | # For production, compiles app with optimizations and places it in bin/.
154 | shards build --production
155 | ```
156 |
157 | To build the application in a more manual way, skip dependency checking, and control more of the options, you would run:
158 |
159 | ```shell
160 | # For production, compiles app with optimizations and places it in bin/.
161 | # Crystal by default compiles using 8 threads (tune if needed with --threads NUM)
162 | crystal build --no-debug --release --verbose -t -s -p -o bin/ src/.cr
163 | ```
164 |
165 | As mentioned, for faster build speed, development versions are compiled without the `--release` flag. With the `--release` flag the compilation takes noticeably longer, but the resulting binary has incredible performance.
166 |
167 | Thanks to Crystal's compiler implementation, only the parts actually used are added to the executable. Listing dependencies in `shard.yml` or even using `require`s in your program will generally not affect what is compiled in.
168 |
169 | Crystal caches partial results of the compilation (*.o files etc.) under `~/.cache/crystal/` for faster subsequent builds. This directory is also where temporary binaries are placed when one runs programs with `crystal [run]` rather than `crystal build`.
170 |
171 | Sometimes building the app will fail on the C level because of missing header files or libraries. If Crystal doesn't print the actual C error, it will at least print the compiler line that caused it.
172 |
173 | The best way to see the actual error from there is to copy-paste the command printed and run it manually in the terminal. The error will be shown and from there the cause and solution will be determined easily. Usually some library or header files will be missing, such as those mentioned above in [Running the App](#running_the_app).
174 |
175 | There are some issues with the `libgc` library here and there. Sometimes it helps to reinstall the system's package `libgc-dev`.
176 |
177 | # REPL
178 |
179 | Often times, it is very useful to enter an interactive console (think of IRB shell) with all application classes initialized etc. In Ruby this would be done with IRB or with a command like `rails console`.
180 |
181 | Crystal does not have a free-form [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop), but you can save and execute scripts in the context of the application. One way to do it is via command `amber x [filename]`. This command will allow you to type or edit the contents, and then execute the script.
182 |
183 | Another, possibly more flexible way to do it is via standalone REPL-like tools [cry](https://github.com/elorest/cry) or [icr](https://github.com/crystal-community/icr). `cry` began as an experiment and a predecessor to `amber x`, but now offers additional functionality such as repeatedly editing and running the script if `cry -r` is invoked.
184 |
185 | In any case, running a script "in application context" simply means requiring `config/application.cr` (and through it, `config/**`). Therefore, be sure to list all your requires in `config/application.cr` so that everything works as expected, and if you are using `cry` or `icr`, have `require "./config/application"` as the first line.
186 |
187 | # File Structure
188 |
189 | So, at this point you might be wanting to know what's placed where in an Amber application. The default structure looks like this:
190 |
191 | ```
192 | ./config/ - All configuration, detailed in subsequent lines:
193 | ./config/initializers/ - Initializers (code you want executed at the very beginning)
194 | ./config/environments/ - Environment-specific YAML configurations (development, production, test)
195 | ./config/application.cr - Main configuration file for the app. Generally not touched (apart
196 | from adding "require"s to the top) because most of the config
197 | settings are specified in YAML files in config/environments/
198 | ./config/webpack/ - Webpack (asset bundler) configuration
199 | ./config/routes.cr - All routes
200 |
201 | ./db/migrations/ - All DB migration files (created with "amber g migration ...")
202 |
203 | ./public/ - The "public" directory for static files
204 | ./public/dist/ - Directory inside "public" for generated files and bundles
205 | ./public/dist/images/
206 |
207 | ./src/ - Main source directory, with .cr being the main file
208 | ./src/controllers/ - All controllers
209 | ./src/models/ - All models
210 | ./src/views/layouts/ - All layouts
211 | ./src/views/ - All views
212 | ./src/views/home/ - Views for HomeController (the app's "/" path)
213 | ./src/locales/ - Toplevel directory for locale (translation) files named [lang].yml
214 | ./src/assets/ - Static assets which will be bundled and placed into ./public/dist/
215 | ./src/assets/fonts/
216 | ./src/assets/images/
217 | ./src/assets/javascripts/
218 | ./src/assets/stylesheets/
219 |
220 | ./spec/ - Toplevel directory for test files named "*_spec.cr"
221 | ```
222 |
223 | I prefer to have some of these directories accessible directly in the root directory of the application and to have the config directory aliased to `etc`, so I run:
224 |
225 | ```
226 | ln -sf config etc
227 | ln -sf src/assets
228 | ln -sf src/controllers
229 | ln -sf src/models
230 | ln -sf src/views
231 | ln -sf src/views/layouts
232 |
233 | ```
234 |
235 | # Database Commands
236 |
237 | Amber provides a group of subcommands under `amber db` to allow working with the database. The simple commands you will most probably want to run first just to see things working are:
238 |
239 | ```shell
240 | amber db create
241 | amber db status
242 | amber db version
243 | ```
244 |
245 | Before these commands will work, you will need to configure database
246 | credentials as follows:
247 |
248 | First, create a user to access the database. For PostgreSQL, this is done by invoking something like:
249 |
250 | ```shell
251 | $ sudo su - postgres
252 | $ createuser -dElPRS myuser
253 | Enter password for new role:
254 | Enter it again:
255 | ```
256 |
257 | Then, edit `config/environments/development.yml` and configure "database_url:" to match your settings. If nothing else, the part that says "postgres:@" should be replaced with "yourusername:yourpassword@".
258 |
259 | And then try the database commands from the beginning of this section.
260 |
261 | Please note that for the database connection to succeed, everything must be set up correctly — hostname, port, username, password, and database name must be valid, the database server must be accessible, and the database must actually exist unless you are invoking `amber db create` to create it. In case of *any error in any of these requirements*, the error message will be terse and just say "Connection unsuccessful: ". The solution is simple, though - simply use the printed database_url to manually attempt a connection to the database with the same parameters, and the problem will most likely quickly reveal itself.
262 |
263 | (If you are sure that the username and password are correct and that the database server is accessible, then the most common problem is that the database does not exist yet, so you should run `amber db create` as the first command to create it.)
264 |
265 | Please note that the environment files for non-production environment are given in plain text. Environment file for the production environment is encrypted for additional security and can be seen or edited by invoking `amber encrypt`.
266 |
267 | # Pipes and Pipelines
268 |
269 | In very simple frameworks it could suffice to directly map incoming requests to methods in the application, call them, and return their output to the user.
270 |
271 | More elaborate application frameworks like Amber provide many more features and flexibility, and allow pluggable components to be inserted and executed in the chosen order before the actual controller method is invoked to handle the request.
272 |
273 | These components are in general terminology called "middleware". Crystal calls them "handlers", and Amber calls them "pipes". In any case, in Amber applications they all refer to the same thing — classes that `include` Crystal's module [HTTP::Handler](https://crystal-lang.org/api/0.24.2/HTTP/Handler.html) and that implement method `def call(context)`. (So in Amber, this functionality is based on Crystal's HTTP server's built-in support for handlers/pipes.)
274 |
275 | Pipes work in such a way that invoking the pipes is not automatic, but each pipe must explicitly invoke `call_next(context)` to call the next pipe in a row. This is actually desirable because it makes it possible to call the next pipe at exactly the right place in the code where you want it and if you want it — at the beginning, in the middle, or at the end of your current pipe's code, or not at all.
276 |
277 | The request and response data that pipes need in order to run and do anything meaningful is passed as the first argument to every pipe, and is by convention named "context".
278 |
279 | Context persists for the duration of the request and is the place where data that should be shared/carried between pipes should be saved. Amber extends the default [HTTP::Server::Context](https://crystal-lang.org/api/0.24.2/HTTP/Server/Context.html) class with many additional fields and methods as can be seen in [router/context.cr](https://github.com/amberframework/amber/blob/master/src/amber/router/context.cr) and [extensions/http.cr](https://github.com/amberframework/amber/blob/master/src/amber/extensions/http.cr).
280 |
281 | Handlers or pipes are not limited in what they can do. It is normal that they sometimes stop execution and return an error, or fulfil the request on their own without even passing the request through to the controller. Examples of such pipes are [CSRF](https://github.com/amberframework/amber/blob/master/src/amber/pipes/csrf.cr) which stops execution if CSRF token is incorrect, or [Static](https://github.com/amberframework/amber/blob/master/src/amber/pipes/static.cr) which autonomously handles delivery of static files.
282 |
283 | Using pipes promotes code reuse and is a nice way to plug various standard or custom functionality in the request serving process without requiring developers to duplicate code or include certain parts of code in every controller action.
284 |
285 | Additionally, in Amber there exists a concept of "pipelines". Pipelines are logical groups of pipes. The discussion about them continues in the next section.
286 |
287 | # Routes, Controller Methods, and Responses
288 |
289 | Before expanding the information on pipes and pipelines, let's explain the concept of routes.
290 |
291 | Routes connect incoming requests (HTTP methods and URL paths) to specific controllers and controller methods in your application. Routes are checked in the order they are defined and the first route that matches wins.
292 |
293 | All routes belong to a certain pipeline (like "web", "api", or similar). When a route matches, Amber simply executes all pipes in the pipeline under which that route has been defined. The last pipe in every pipeline is implicitly the pipe named "[Controller](https://github.com/amberframework/amber/blob/master/src/amber/pipes/controller.cr)". That's the pipe which actually looks into the original route, instantiates the specified controller, and calls the specified method in it. Please note that this is currently non-configurable — the controller pipe is always automatically added as the last pipe in the pipeline and it is executed unless processing stops in one of the earlier pipes.
294 |
295 | The configuration for pipes, pipelines, and routes is found in the file `config/routes.cr`. This file invokes the same `configure` block that `config/application.cr` does, but since routes configuration is important and can also be lengthy and complex, Amber keeps it in a separate file.
296 |
297 | Amber includes commands `amber routes` and `amber pipelines` to display route and pipeline configurations. By default, the output for routes looks like the following:
298 |
299 | ```shell
300 | $ amber routes
301 |
302 |
303 | ╔══════╦═══════════════════════════╦════════╦══════════╦═══════╦═════════════╗
304 | ║ Verb | Controller | Action | Pipeline | Scope | URI Pattern ║
305 | ╠──────┼───────────────────────────┼────────┼──────────┼───────┼─────────────╣
306 | ║ get | HomeController | index | web | | / ║
307 | ╠──────┼───────────────────────────┼────────┼──────────┼───────┼─────────────╣
308 | ║ get | Amber::Controller::Static | index | static | | /* ║
309 | ╚══════╩═══════════════════════════╩════════╩══════════╩═══════╩═════════════╝
310 |
311 |
312 | ```
313 |
314 | From the first line of the output we see that a "GET /" request will cause all pipes in the pipeline "web" to be executed, and then
315 | `HomeController.new.index` method will be called.
316 |
317 | In the `config/routes.cr` code, this is simply achieved with the line:
318 |
319 | ```crystal
320 | routes :web do
321 | get "/", HomeController, :index
322 | end
323 | ```
324 |
325 | The return value of the controller method is returned as response body to the client.
326 |
327 | As another example, the following definition would cause a POST request to "/registration" to result in invoking `RegistrationController.new.create`:
328 |
329 | ```
330 | post "/registration", RegistrationController, :create
331 | ```
332 |
333 | By convention, standard HTTP verbs (GET/HEAD, POST, PUT/PATCH, and DELETE) should be routed to standard-named methods on the controllers — `show`, `create`, `update`, and `destroy`. However, there is nothing preventing you from routing URLs to any methods you want in the controllers, such as we've seen with `index` above.
334 |
335 | Websocket routes are supported too.
336 |
337 | The DSL language specific to `config/routes.cr` file is defined in [dsl/router.cr](https://github.com/amberframework/amber/blob/master/src/amber/dsl/router.cr) and [dsl/server.cr](https://github.com/amberframework/amber/blob/master/src/amber/dsl/server.cr).
338 |
339 | It gives you the following top-level commands/blocks:
340 |
341 | ```
342 | # Define a pipeline
343 | pipeline :name do
344 | # ... list of pipes ...
345 | end
346 |
347 | # Group a set of routes
348 | routes :pipeline_name, "/optional_path_prefix" do
349 | # ... list of routes ...
350 | end
351 | ```
352 |
353 | This is used in practice in the following way in `config/routes.cr`:
354 |
355 | ```crystal
356 | Amber::Server.configure do |app|
357 | pipeline :web do
358 | # Plug is the method used to connect a pipe (middleware).
359 | # A plug accepts an instance of HTTP::Handler.
360 | plug Amber::Pipe::Logger.new
361 | end
362 |
363 | routes :web do
364 | get "/", HomeController, :index # Routes "GET /" to HomeController.new.index
365 | post "/test", PageController, :test # Routes "POST /test" to PageController.new.test
366 | end
367 | end
368 | ```
369 |
370 | Within "routes" blocks the following commands are available:
371 |
372 | ```crystal
373 | get, post, put (or patch), delete, options, head, trace, connect, websocket, resources
374 | ```
375 |
376 | Most of these actions correspond to the respective HTTP methods; `websocket` defines websocket routes; and `resources` is a macro defined as:
377 |
378 | ```crystal
379 | macro resources(resource, controller, only = nil, except = nil)
380 | ```
381 |
382 | Unless `resources` is confined with arguments `only` or `except`, it will automatically route `get`, `post`, `put/patch`, and `delete` HTTP methods to methods `index`, `show`, `new`, `edit`, `create`, `update`, and `destroy` on the controller.
383 |
384 | Please note that it is not currently possible to define a different behavior for GET and HEAD HTTP methods on the same path. If a GET is defined, it will also automatically add the matching HEAD route. Specifying HEAD route manually would then result in two HEAD routes existing for the same path and trigger `Amber::Exceptions::DuplicateRouteError`.
385 |
386 | # Views
387 |
388 | Information about views can be summarized in the following bullet points:
389 |
390 | - Views in an Amber project are located under the toplevel directory `src/views/`
391 | - Views are typically rendered using `render()`
392 | - The first argument given to `render()` is the template name (e.g. `render("index.slang")`)
393 | - `render("index.slang")` will look for a view named `src/views//index.slang`
394 | - `render("./abs/or/rel/path.slang")` will look for a template in that specific path
395 | - There is no unnecessary magic applied to template names — names specified are the names that will be looked up on disk
396 | - If you are not rendering a partial, by default the template will be wrapped in a layout
397 | - If the layout name isn't specified, the default layout will be `views/layouts/application.slang`
398 | - To render a partial, use `render( partial: "_name.ext")`
399 | - Partials begin with "\_" by convention, but that is not required. If they are named with "\_", then the "\_" must be mentioned as part of the name
400 | - Templates are read from disk and compiled into the application at compile time. This makes them fast to access and also read-only which is a useful side-benefit
401 |
402 | The `render` macro is usually invoked at the end of the controller method. This makes its return value be the return value of the controller method as a whole, and as already mentioned, the controller method's return value is returned to the client as response body.
403 |
404 | It is also important to know that `render` is a macro and that views are rendered directly (in-place) as part of the controller method.
405 | This results in a very interesting property — since `render` executes directly in the controller method, it sees all local variables in it and view data does not have to be passed via instance variables. This particular aspect is explained in more detail further below under [Variables in Views](#variables_in_views).
406 |
407 | ## Template Languages
408 |
409 | In the introduction we've mentioned that Amber supports two template languages — [slang](https://github.com/jeromegn/slang) (default) and [ecr](https://crystal-lang.org/api/0.21.1/ECR.html).
410 |
411 | That's because Amber ships with a minimal working layout (a total of 3 files) in those languages, but there is nothing preventing you from using any other languages if you have your own templates or want to convert existing ones.
412 |
413 | Amber's default rendering engine is [Kilt](https://github.com/jeromegn/kilt), so all languages supported by Kilt should be usable out of the box. Amber does not make assumptions about the template language used; the view file's extension will determine which parser will be invoked (e.g. ".ecr" for ecr, ".slang" for slang).
414 |
415 | ### Liquid Template Language
416 |
417 | The original [Kilt](https://github.com/jeromegn/kilt) repository now has support for the Liquid template language.
418 |
419 | Please note, however, that Liquid as a template language comes with non-typical requirements — primarily, it requires a separate store ("context") for user data which is to be available in templates, and also it does not allow arbitrary functions, objects, object methods, and data types to be used in its templates.
420 |
421 | As such, Amber's principle of rendering the templates directly inside controller methods (and thus making all local variables automatically available in views) cannot be used because Liquid's context is separate and local variables are not there.
422 |
423 | Also, Liquid's implementation by default tries to be helpful and it automatically creates a new context. It copies all instance variables (@ivars) from the current object into the newly created context, which again cannot be used with Amber for two reasons.
424 | First, because the copying does not work for data other than basic types (e.g. saying `@process = Process` does not make `{{ process.pid }}` usable in a Liquid template). Second, because Amber's controllers already contain various instance variables that should not or can not be serialized, so even simply saying `render("index.liquid")` would result in a compile-time error in Crystal even if the template itself was empty.
425 |
426 | Also, Amber's `render` macro does not accept extra arguments, so a custom context can't be passed to Kilt and from there to Liquid.
427 |
428 | Therefore, the best approach to work with Liquid in Amber is to create a custom context, populate it with desired values, and then invoke `Kilt.render` macro directly (without using Amber's `render` macro). The pull request [#610](https://github.com/amberframework/amber/pull/610) to make rendering engines includable/choosable at will was refused by the Amber project, so if you are bothered that the default `render` macro is present in your application even though you do not use it, simply comment the line `include Helpers::Render` in Amber's [controller/base.cr](https://github.com/amberframework/amber/blob/master/src/amber/controller/base.cr).
429 |
430 | Please also keep in mind not to use the name "context" for the variable that will hold Liquid's context, because that would take precedence over the `context` getter that already exists on the controllers and is used to access `HTTP::Server::Context` object.
431 |
432 | So, altogether, a working example for rendering Liquid templates in Amber would look like the following (showing the complete controller code for clarity):
433 |
434 | ```
435 | class HomeController < ApplicationController
436 | def index
437 | ctx = Liquid::Context.new
438 | ctx.set "process", { "pid" => Process.pid }
439 |
440 | # The following would render src/views/[controller]/index.liquid
441 | Kilt.render "index.liquid", ctx
442 |
443 | # The following would render specified path, relative to app base directory
444 | Kilt.render "src/views/myview.liquid", ctx
445 | end
446 | end
447 | ```
448 |
449 | # Logging
450 |
451 | Amber logger (based on standard Crystal's class `Logger`) is initialized as soon as `require "amber"` is called, as part of reading the settings and initializing the environment.
452 |
453 | The variable containing the logger is `Amber.settings.logger`. For convenience, it is also available as `Amber.logger`. In the context of a Controller, it is also available as simply `logger`.
454 |
455 | Controllers and views execute in the same class (the class of the controller), so calling the following anywhere in the controller or views will produce the expected log line:
456 |
457 | ```crystal
458 | logger.info "Informational Message"
459 | ```
460 |
461 | Log levels available are `debug`, `info`, `warn`, `error`, `fatal`, and `unknown`.
462 |
463 | The second, optional parameter passed to the log method will affect the displayed name of the subsystem from which the message originated. For example:
464 |
465 |
466 | ```crystal
467 | logger.warn "Starting up", "MySystem"
468 | ```
469 |
470 | would result in the log line:
471 |
472 | ```
473 | 03:17:04 MySystem | (WARN) Starting up
474 | ```
475 |
476 | In you still need a customized logger for special cases or purposes, please create a separate `Logger.new` yourself.
477 |
478 | # Parameter Validation
479 |
480 | First of all, Amber framework considers query and body params equal and makes them available to the application in the same, uniform way.
481 |
482 | Second of all, the params handling in Amber is not programmed in a truly clean and non-overlapping way, but the description here should be clear to understand.
483 |
484 | There are just three important methods to have in mind — `params.validation {...}` which defines validation rules, `params.valid?` which returns whether parameters pass validation, and `params.validate!` which requires that parameters pass validation or raises an error.
485 |
486 | A simple validation process in a controller could look like this (showing the whole Controller class for completeness):
487 |
488 | ```crystal
489 | class HomeController < ApplicationController
490 | def index
491 | params.validation do
492 | required(:name) { |n| n.size > 6 } # Name must have at least 6 characters
493 | optional(:phone) { |n| n.phone? } # Phone must look like a phone number
494 | end
495 |
496 | "Params valid: #{params.valid?.to_s}
Name is: #{params[:name]}"
497 | end
498 | end
499 | ```
500 |
501 | (Extensions to the String class such as `phone?` seen above come especially handy for writing validations. Please see [Extensions](#extensions) below for the complete list of built-in extensions available.)
502 |
503 | With this foundation in place, let's take a step back to explain the underlying principles and also expand the full description:
504 |
505 | As already mentioned above, for every incoming request, Amber uses data from `config/routes.cr` to determine which controller and method in it should handle the request. Then it instantiates that controller (calls `new` on it), and because all controllers inherit from `ApplicationController` (which in turn inherits from `Amber::Controller:Base`), the following code is executed as part of initialize:
506 |
507 | ```crystal
508 | protected getter params : Amber::Validators::Params
509 |
510 | def initialize(@context : HTTP::Server::Context)
511 | @params = Amber::Validators::Params.new(context.params)
512 | end
513 | ```
514 |
515 | In other words, `params` object is initialized using raw params passed with the user's request (i.e. `context.params`). From there, it is important to know that `params` object contains 4 important variables (getters):
516 |
517 | 1. `params.raw_params` - this is a reference to hash `context.params` created during initialize, and all methods invoked on `params` directly (such as `[]`, `[]?`, `[]=`, `add`, `delete`, `each`, `fetch`, etc.) are forwarded to this object. Please note that this is a reference and not a copy, so all modifications made there also affect `context.params`
518 | 1. `params.rules` - this is initially an empty list of validation rules. It is filled in as validation rules are defined using `params.validation {...}`
519 | 1. `params.params` - this is a hash of key=value parameters, but only those that were mentioned in the validation rules and that passed validation when `valid?` or `validate!` were called. This list is re-initialized on every call to `valid?` or `validate!`. Using this hash ensures that you only work with validated/valid parameters
520 | 1. `params.errors` - this is a list of all eventual errors that have ocurred during validation with `valid?` or `validate!`. This list is re-initialized on every call to `valid?` or `validate!`
521 |
522 | And this is basically all there is to it. From here you should have a complete understanding how to work with params validation in Amber.
523 |
524 | (TODO: Add info on model validations)
525 |
526 | # Static Pages
527 |
528 | It can be pretty much expected that a website will need a set of simple, "static" pages. Those pages are served by the application, but mostly don't use a database nor any complex code. Such pages might include About and Contact pages, Terms amd Conditions, and so on. Making this work is trivial and will serve as a great example.
529 |
530 | Let's say that, for simplicity and logical grouping, we want all "static" pages to be served by a controller we will create, named "PageController". We will group all these "static" pages under a common web-accessible prefix of /page/, and finally we will route page requests to PageController's methods. Because these pages won't be backed by objects, we won't need models or anything else other than one controller method and one view per each page.
531 |
532 | Let's create the controller:
533 |
534 | ```shell
535 | amber g controller page
536 | ```
537 |
538 | Then, we edit `config/routes.cr` to link e.g. URL "/page/about" to method about() in PageController. We do this inside the "routes :web" block:
539 |
540 | ```
541 | routes :web, "/page" do
542 | ...
543 | get "/about", PageController, :about
544 | ...
545 | end
546 | ```
547 |
548 | Then, we edit the controller and actually add method about(). This method can just directly return a string in response, or it can render a view. In any case, the return value from the method will be returned as the response body to the client, as usual.
549 |
550 | ```shell
551 | $ vi src/controllers/page_controller.cr
552 |
553 | # Inside the file, we add:
554 |
555 | def about
556 | # "return" can be omitted here. It is included for clarity.
557 | render "about.ecr"
558 | end
559 | ```
560 |
561 | Since this is happening in the "page" controller, the view directory for finding the templates will default to `src/views/page/`. We will create the directory and the file "about.ecr" in it:
562 |
563 | ```shell
564 | $ mkdir -p src/views/page/
565 | $ vi src/views/page/about.ecr
566 |
567 | # Inside the file, we add:
568 |
569 | Hello, World!
570 | ```
571 |
572 | Because we have called render() without additional arguments, the template will default to being rendered within the default application layout, `views/layouts/application.cr`.
573 |
574 | And that is it! The request for `/page/about` will reach the router, the router will invoke `PageController.new.about()`, that method will render template `src/views/page/about.ecr` in the context of layout `views/layouts/application.cr`, the result of rendering will be a full page with content `Hello, World!` in the body, and that result will be returned to the client as response body.
575 |
576 | # Variables in Views
577 |
578 | As mentioned, in Amber, templates are compiled and rendered directly in the context of the methods that call `render()`. Those are typically the controller methods themselves, and it means you generally do not need instance variables for passing the information from controllers to views.
579 |
580 | Any variable you define in the controller method, instance or local, is directly visible in the template. For example, let's add the date and time and display them on our "About" page created in the previous step. The controller method and the corresponding view template would look like this:
581 |
582 | ```shell
583 | $ vi src/controllers/page_controller.cr
584 |
585 | def about
586 | time = Time.now
587 | render "about.ecr"
588 | end
589 |
590 | $ vi src/views/page/about.ecr
591 |
592 | Hello, World! The time is now <%= time %>.
593 | ```
594 |
595 | To further confirm that the templates also implicitly run in the same controller objectthat handled the request, you could place e.g. "<%= self.class %> in the above example; the response would be "PageController". So in addition to seeing the method's local variables, it means that all instance variables and methods existing on the controller object are readily available in the templates as well.
596 |
597 | # More on Database Commands
598 |
599 | ## Micrate
600 |
601 | Amber relies on the shard "[micrate](https://github.com/amberframework/micrate)" to perform migrations. The command `amber db` uses "micrate" unconditionally. However, some of all the possible database operations are only available through `amber db` and some are only available through invoking `micrate` directly. Therefore, it is best to prepare the application for using both `amber db` and `micrate`.
602 |
603 | Micrate is primarily a library so a small piece of custom code is required to provide the minimal `micrate` executable for a project. This is done by placing the following in `src/micrate.cr` (the example is for PostgreSQL but could be trivially adapted to MySQL or SQLite):
604 |
605 | ```crystal
606 | #!/usr/bin/env crystal
607 | require "amber"
608 | require "micrate"
609 | require "pg"
610 |
611 | Micrate::DB.connection_url = Amber.settings.database_url
612 | Micrate.logger = Amber.settings.logger.dup
613 | Micrate.logger.progname = "Micrate"
614 |
615 | Micrate::Cli.run
616 | ```
617 |
618 | And by placing the following in `shard.yml` under `targets`:
619 |
620 | ```
621 | targets:
622 | micrate:
623 | main: src/micrate.cr
624 | ```
625 |
626 | From there, running `shards build micrate` would build `bin/micrate` which you could use as an executable to access micrate's functionality directly. Please run `bin/micrate -h` to see an overview of its commands.
627 |
628 | Please note that the described procedure sets up `bin/micrate` and `amber db` in a compatible way so these commands can be used cooperatively and interchangeably.
629 |
630 | To have your database migrations run with different credentials than your regular Amber app, simply create new environments in `config/environments/` and prefix your command lines with `AMBER_ENV=...`. For example, you could copy and modify `config/environments/development.yml` into `config/environments/development_admin.yml`, change the credentials as appropriate, and then run migrations as admin using `AMBER_ENV=development_admin ./bin/amber db migrate`.
631 |
632 | ## Custom Migrations Engine
633 |
634 | While `amber db` unconditionally depends on "micrate", that's the only place where it makes an assumption about the migrations engine used.
635 |
636 | To use a different migrations engine, such as [migrate.cr](https://github.com/vladfaust/migrate.cr), simply perform all database migration work using the engine's native commands instead of using `amber db`. No other adjustments are necessary, and Amber won't get into your way.
637 |
638 | # Internationalization (I18n)
639 |
640 | Amber uses Amber's native shard [citrine-18n](https://github.com/amberframework/citrine-i18n) to provide translation and localization. Even though the shard has been authored by the Amber Framework project, it is Amber-independent and can be used to initialize I18n and determine the visitor's preferred language in any application based on Crystal's HTTP::Server.
641 |
642 | That shard in turn depends on the shard [i18n.cr](https://github.com/TechMagister/i18n.cr) to provide the actual translation and localization functionality.
643 |
644 | The internationalization functionality in Amber is enabled by default. Its setup, initialization, and use basically consist of the following:
645 |
646 | 1. Initializer file `config/initializers/i18n.cr` where basic I18n settings are defined and `I18n.init` is invoked
647 | 1. Locale files in `src/locales/` and subdirectories where settings for both translation and localization are contained
648 | 1. Pipe named `Citrine::I18n::Handler` which is included in `config/routes.cr` and which detects the preferred language for every request based on the value of the request's HTTP header "Accept-Language"
649 | 1. Controller helpers named `t()` and `l()` which provide shorthand access for methods `::I18n.translate` and `::I18n.localize`
650 |
651 | Once the pipe runs on the incoming request, the current request's locale is set in the variable `::I18n.locale`. The value is not stored or copied in any other location and it can be overriden in runtime in any way that the application would require.
652 |
653 | For a locale to be used, it must be requested (or be the default) and exist anywhere under the directory `./src/locales/` with the name `[lang].yml`. If nothing can be found or matched, the locale value will default to "en".
654 |
655 | From there, invoking `t()` and `l()` will perform translation and localization according to the chosen locale. Since these two methods are direct shorthands for methods `::I18n.translate` and `::I18n.localize`, all their usage information and help should be looked up in [i18n.cr's README](https://github.com/TechMagister/i18n.cr).
656 |
657 | In a default Amber application there is a sample localization file `src/locales/en.yml` with one translated string ("Welcome to Amber Framework!") which is displayed as the title on the default project homepage.
658 |
659 | In the future, the default/built-in I18n functionality in Amber might be expanded to automatically organize translations and localizations under subdirectories in `src/locales/` when generators are invoked, just like it is already done for e.g. files in `src/views/`. (This functionality already exists in i18n.cr as explained in [i18n.cr's README](https://github.com/TechMagister/i18n.cr), but is not yet used by Amber.)
660 |
661 | # Responses
662 |
663 | ## Responses with Different Content-Type
664 |
665 | If you want to provide a different format (or a different response altogether) from the controller methods based on accepted content types, you can use `respond_with` from `Amber::Helpers::Responders`.
666 |
667 | Our `about` method from the previous example could be modified in the following way to respond with either HTML or JSON:
668 |
669 | ```crystal
670 | def about
671 | respond_with do
672 | html render "about.ecr"
673 | json name: "John", surname: "Doe"
674 | end
675 | end
676 | ```
677 |
678 | Supported format types are `html`, `json`, `xml`, and `text`. For all the available methods and arguments, please see [controller/helpers/responders.cr](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/responders.cr).
679 |
680 | ## Error Responses
681 |
682 | ### Manual Error Responses
683 |
684 | In any pipe or controller action, you might want to manually return a simple error to the user. This typically means returning an HTTP error code and a short error message, even though you could just as easily print complete pages into the return buffer and return an error code.
685 |
686 | To stop a request during execution and return an error, you would do it this way:
687 |
688 | ```
689 | if some_condition_failed
690 | Amber.logger.error "Error! Returning Bad Request"
691 |
692 | # Status and any headers should be set before writing response body
693 | context.response.status_code = 400
694 |
695 | # Finally, write response body
696 | context.response.puts "Bad Request"
697 |
698 |
699 | # Another way to do the same and respond with a text/plain error
700 | # is to use Crystal's respond_with_error():
701 | context.response.respond_with_error("Bad Request", 400)
702 |
703 | return
704 | end
705 | ```
706 |
707 | Please note that you must use `context.response.puts` or `context.response<<` to print to the output buffer in case of errors. You cannot set the error code and then call `return "Bad Request"` because the return value will not be added to response body if HTTP code is not 2xx.
708 |
709 | ### Error Responses via Error Pipe
710 |
711 | The above example for manually returning error responses does not involve raising any Exceptions — it simply consists of setting the status and response body and returning them to the client.
712 |
713 | Another approach to returning errors consists in using Amber's error subsystem. It automatically provides you with a convenient way to raise Exceptions and return them to the client properly wrapped in application templates etc.
714 |
715 | This method relies on the fact that pipes call next pipes in a row explicitly, and so the method call chain is properly established. In turn, this means that e.g. raising an exception in your controller can be rescued by an earlier pipe in a row that wrapped the call to `call_next(context)` inside a `begin...rescue` block.
716 |
717 | Amber contains a generic pipe named "Errors" for handling errors. It is activated by using the line `plug Amber::Pipe::Error.new` in your `config/routes.cr`.
718 |
719 | To be able to extend the list of errors or modify error templates yourself, you should first run `amber g error` to copy the relevant files to your application. In principle, running this command will get you the files `src/pipes/error.cr`, `src/controllers/error_controller.cr`, and `src/views/error/`, all of which can be modified to suit your needs.
720 |
721 | To see the error subsystem at work, you could now do something as simple as:
722 |
723 | ```crystal
724 | class HomeController < ApplicationController
725 | def index
726 | raise Exception.new "No pass!"
727 | render("index.slang")
728 | end
729 | end
730 | ```
731 |
732 | And then visit [http://localhost:3000/](http://localhost:3000/). You would see a HTTP 500 (Internal Server Error) containing the specified error message, but wrapped in an application template rather than printed plainly like the most basic HTTP errors.
733 |
734 | # Assets Pipeline
735 |
736 | In an Amber project, raw assets are in `src/assets/`:
737 |
738 | ```shell
739 | app/src/assets/
740 | app/src/assets/fonts
741 | app/src/assets/images
742 | app/src/assets/images/logo.svg
743 | app/src/assets/javascripts
744 | app/src/assets/javascripts/main.js
745 | app/src/assets/stylesheets
746 | app/src/assets/stylesheets/main.scss
747 |
748 | ```
749 |
750 | At build time, all these are processed and placed under `public/dist/`.
751 | The JS resources are bundled to `main.bundle.js` and CSS resources are bundled to `main.bundle.css`.
752 |
753 | [Webpack](https://webpack.js.org/) is used for asset management.
754 |
755 | To include additional .js or .css/.scss files you would generally add `import "../../file/path";` statements to `src/assets/javascripts/main.js`. You add both JS and CSS includes into `main.js` because webpack only processes import statements in .js files. So you must add the CSS import lines to a .js file, and as a result, this will produce a JS bundle that contains both JS and CSS data in it. Then, webpack's plugin named ExtractTextPlugin (part of default configuration) is used to extract CSS parts into their own bundle.
756 |
757 | The base/common configuration for all this is in `config/webpack/common.js`.
758 |
759 | ## Adding jQuery and jQuery UI
760 |
761 | As an example, we can add the jQuery and jQuery UI libraries to an Amber project.
762 |
763 | Please note that we are going to unpack the jQuery UI zip file directly into `src/assets/javascripts/` even though it contains some CSS and images. This is done because splitting the different asset types out to individual directories would be harder to do and maintain over time (e.g. paths in jQuery UI CSS files pointing to "images/" would no longer work, and updating the version later would be more complex).
764 |
765 | The whole procedure would be as follows:
766 |
767 | ```bash
768 | cd src/assets/javascripts
769 |
770 | # Download jQuery
771 | wget https://code.jquery.com/jquery-3.3.1.js
772 |
773 | # Then download jQuery UI from http://jqueryui.com/download/ to the same/current directory
774 | # and unpack it:
775 | unzip jquery-ui-1.12.1.custom.zip
776 |
777 | # Then edit main.js and add the import lines:
778 | import './jquery-3.3.1.min.js'
779 | import './jquery-ui-1.12.1.custom/jquery-ui.css'
780 | import './jquery-ui-1.12.1.custom/jquery-ui.js'
781 | import './jquery-ui-1.12.1.custom/jquery-ui.structure.css'
782 | import './jquery-ui-1.12.1.custom/jquery-ui.theme.css'
783 |
784 | # And finally, edit ../../../config/webpack/common.js to add jquery resource alias:
785 | resolve: {
786 | alias: {
787 | amber: path.resolve(__dirname, '../../lib/amber/assets/js/amber.js'),
788 | jquery: path.resolve(__dirname, '../../src/assets/javascripts/jquery-3.3.1.min.js')
789 | }
790 | ```
791 |
792 | And that's it. At the next application build (e.g. with `amber watch`) all the mentioned resources and images will be compiled, placed to `public/dist/`, and included in the CSS/JS files.
793 |
794 | ## Resource Aliases
795 |
796 | Sometimes, the code or libraries you include will in turn require other libraries by their generic name, e.g. "jquery". Since a file named "jquery" does not actually exist on disk (or at least not in the location that is searched), this could result in an error such as:
797 |
798 | ```
799 | ERROR in ./src/assets/javascripts/jquery-ui-1.12.1.custom/jquery-ui.js
800 | Module not found: Error: Can't resolve 'jquery' in '.../src/assets/javascripts/jquery-ui-1.12.1.custom'
801 | @ ./src/assets/javascripts/jquery-ui-1.12.1.custom/jquery-ui.js 5:0-26
802 | @ ./src/assets/javascripts/main.js
803 | ```
804 |
805 | The solution is to add resource aliases to webpack's configuration which will instruct it where to find the real files if/when they are referenced by their alias.
806 |
807 | For example, to resolve "jquery", you would add the following to the "resolve" section in `config/webpack/common.js`:
808 |
809 | ```
810 | ...
811 | resolve: {
812 | alias: {
813 | jquery: path.resolve(__dirname, '../../src/assets/javascripts/jquery-3.3.1.min.js')
814 | }
815 | }
816 | ...
817 | ```
818 |
819 | ## CSS Optimization / Minification
820 |
821 | You might want to minimize the CSS that is output to the final CSS bundle.
822 |
823 | To do so you need an entry under "devDependencies" in the project's file `package.json`:
824 |
825 | ```
826 | "optimize-css-assets-webpack-plugin": "^1.3.0",
827 | ```
828 |
829 | And an entry at the top of `config/webpack/common.js`:
830 |
831 | ```
832 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
833 | ```
834 |
835 | And you need to run `npm install` for the plugin to be installed (saved to "node_modules/" subdirectory).
836 |
837 | ## File Copying
838 |
839 | You might also want to copy some of the files from their original location to `public/dist/` without doing any modifications in the process. This is done by adding the following under "devDependencies" in `package.json`:
840 |
841 | ```
842 | "copy-webpack-plugin": "^4.1.1",
843 | ```
844 |
845 | To do so you need following at the top of `config/webpack/common.js`:
846 |
847 | ```
848 | const CopyWebpackPlugin = require('copy-webpack-plugin');
849 | ```
850 |
851 | And the following under "plugins" section down below in the file:
852 |
853 | ```
854 | new CopyWebPackPlugin([
855 | {
856 | from: path.resolve(__dirname, '../../vendor/images/'),
857 | to: path.resolve(__dirname, '../../public/dist/images/'),
858 | ignore: ['.*'],
859 | }
860 | ]),
861 | ```
862 |
863 | And as usual, you need to run `npm install` for the plugin to be installed (saved to "node_modules/" subdirectory).
864 |
865 | ## Asset Management Alternatives
866 |
867 | Maybe it would be useful to replace Webpack with e.g. [Parcel](https://parceljs.org/). (Finding a non-js/non-node/non-npm application for this purpose would be even better; please let me know if you know one.)
868 |
869 | In general it seems it shouldn't be much more complex than replacing the command to run and development dependencies in project's `package.json` file.
870 |
871 | # Advanced Topics
872 |
873 | What follows is a collection of advanced topics which can be read or skipped on an individual basis.
874 |
875 | ## Amber::Controller::Base
876 |
877 | This is the base controller from which all other controllers inherit. Source file is in [controller/base.cr](https://github.com/amberframework/amber/blob/master/src/amber/controller/base.cr).
878 |
879 | On every request, the appropriate controller is instantiated and its initialize() runs. Since this is the base controller, this code runs on every request so you can understand what is available in the context of every controller.
880 |
881 | The content of this controller and the methods it gets from including other modules are intuitive enough to be copied here and commented where necessary:
882 |
883 | ```crystal
884 | require "http"
885 |
886 | require "./filters"
887 | require "./helpers/*"
888 |
889 | module Amber::Controller
890 | class Base
891 | include Helpers::CSRF
892 | include Helpers::Redirect
893 | include Helpers::Render
894 | include Helpers::Responders
895 | include Helpers::Route
896 | include Helpers::I18n
897 | include Callbacks
898 |
899 | protected getter context : HTTP::Server::Context
900 | protected getter params : Amber::Validators::Params
901 |
902 | delegate :logger, to: Amber.settings
903 |
904 | delegate :client_ip,
905 | :cookies,
906 | :delete?,
907 | :flash,
908 | :format,
909 | :get?,
910 | :halt!,
911 | :head?,
912 | :patch?,
913 | :port,
914 | :post?,
915 | :put?,
916 | :request,
917 | :requested_url,
918 | :response,
919 | :route,
920 | :session,
921 | :valve,
922 | :websocket?,
923 | to: context
924 |
925 | def initialize(@context : HTTP::Server::Context)
926 | @params = Amber::Validators::Params.new(context.params)
927 | end
928 | end
929 | end
930 |
931 | ```
932 |
933 | [Helpers::CSRF](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/csrf.cr) module provides:
934 |
935 | ```crystal
936 | def csrf_token
937 | def csrf_tag
938 | def csrf_metatag
939 | ```
940 |
941 | [Helpers::Redirect](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/redirect.cr) module provides:
942 |
943 | ```crystal
944 | def redirect_to(location : String, **args)
945 | def redirect_to(action : Symbol, **args)
946 | def redirect_to(controller : Symbol | Class, action : Symbol, **args)
947 | def redirect_back(**args)
948 | ```
949 |
950 | [Helpers::Render](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/render.cr) module provides:
951 |
952 | ```crystal
953 | LAYOUT = "application.slang"
954 | macro render(template = nil, layout = true, partial = nil, path = "src/views", folder = __FILE__)
955 | ```
956 |
957 | [Helpers::Responders](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/responders.cr) helps control what final status code, body, and content-type will be returned to the client.
958 |
959 | [Helpers::Route](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/route.cr) module provides:
960 |
961 | ```crystal
962 | def action_name
963 | def route_resource
964 | def route_scope
965 | def controller_name
966 | ```
967 |
968 | [Callbacks](https://github.com/amberframework/amber/blob/master/src/amber/dsl/callbacks.cr) module provides:
969 |
970 | ```crystal
971 | macro before_action
972 | macro after_action
973 | ```
974 |
975 | ## Extensions
976 |
977 | Amber adds some very convenient extensions to the existing String and Number classes. The extensions are in the [extensions/](https://github.com/amberframework/amber/tree/master/src/amber/extensions) directory. They are useful in general, but particularly so when writing param validation rules. Here's the listing of currently available extensions:
978 |
979 | For String:
980 |
981 | ```crystal
982 | def str?
983 | def email?
984 | def domain?
985 | def url?
986 | def ipv4?
987 | def ipv6?
988 | def mac_address?
989 | def hex_color?
990 | def hex?
991 | def alpha?(locale = "en-US")
992 | def numeric?
993 | def alphanum?(locale = "en-US")
994 | def md5?
995 | def base64?
996 | def slug?
997 | def lower?
998 | def upper?
999 | def credit_card?
1000 | def phone?(locale = "en-US")
1001 | def excludes?(value)
1002 | def time_string?
1003 |
1004 | ```
1005 |
1006 | For Number:
1007 |
1008 | ```crystal
1009 | def positive?
1010 | def negative?
1011 | def zero?
1012 | def div?(n)
1013 | def above?(n)
1014 | def below?(n)
1015 | def lt?(num)
1016 | def self?(num)
1017 | def lteq?(num)
1018 | def between?(range)
1019 | def gteq?(num)
1020 |
1021 | ```
1022 |
1023 | ## Shards
1024 |
1025 | Amber and all of its components depend on the following shards:
1026 |
1027 | ```
1028 | --------SHARD--------------------SOURCE---DESCRIPTION------------------------------------------------------
1029 | ------- Web, Routing, Templates, Mailers, Plugins ---------------------------------------------------------
1030 | require "amber" AMBER Amber itself
1031 | require "amber_router" AMBER Request router implementation
1032 | require "citrine-18n" AMBER Translation and localization
1033 | require "http" CRYSTAL Lower-level supporting HTTP functionality
1034 | require "http/client" CRYSTAL HTTP Client
1035 | require "http/headers" CRYSTAL HTTP Headers
1036 | require "http/params" CRYSTAL Collection of HTTP parameters and their values
1037 | require "http/server" CRYSTAL HTTP Server
1038 | require "http/server/handler" CRYSTAL HTTP Server's support for "handlers" (middleware)
1039 | require "quartz_mailer" AMBER Sending and receiving emails
1040 | require "email" EXTERNAL Simple email sending library
1041 | require "teeplate" AMBER Rendering multiple template files
1042 |
1043 | ------- Databases and ORM Models --------------------------------------------------------------------------
1044 | require "big" EXTERNAL BigRational for numeric. Retains precision, requires LibGMP
1045 | require "db" CRYSTAL Common DB API
1046 | require "pool/connection" CRYSTAL Part of Crystal's common DB API
1047 | require "granite_orm/adapter/<%- @database %>" AMBER Granite's DB-specific adapter
1048 | require "micrate" EXTERNAL Database migration tool
1049 | require "mysql" CRYSTAL MySQL connector
1050 | require "pg" EXTERNAL PostgreSQL connector
1051 | require "redis" EXTERNAL Redis client
1052 | require "sqlite3" EXTERNAL SQLite3 bindings
1053 |
1054 | ------- Template Rendering --------------------------------------------------------------------------------
1055 | require "crikey" EXTERNAL Template language, Data structure view, inspired by Hiccup
1056 | require "crustache" EXTERNAL Template language, {{Mustache}} for Crystal
1057 | require "ecr" CRYSTAL Template language, Embedded Crystal (ECR)
1058 | require "kilt" EXTERNAL Generic template interface
1059 | require "kilt/slang" EXTERNAL Kilt support for Slang template language
1060 | require "liquid" EXTERNAL Template language, used by Amber for recipe templates
1061 | require "slang" EXTERNAL Template language, inspired by Slim
1062 | require "temel" EXTERNAL Template language, extensible Markup DSL
1063 |
1064 | ------- Command Line, Logs, and Output --------------------------------------------------------------------
1065 | require "cli" EXTERNAL Support for building command-line interface applications
1066 | require "colorize" CRYSTAL Changing colors and text decorations
1067 | require "logger" CRYSTAL Simple but sophisticated logging utility
1068 | require "optarg" EXTERNAL Parsing command-line options and arguments
1069 | require "option_parser" CRYSTAL Command line options processing
1070 | require "shell-table" EXTERNAL Creating text tables in command line terminal
1071 |
1072 | ------- Formats, Protocols, Digests, and Compression ------------------------------------------------------
1073 | require "digest/md5" CRYSTAL MD5 digest algorithm
1074 | require "html" CRYSTAL HTML escaping and unescaping methods
1075 | require "jasper_helpers" AMBER Helper functions for working with HTML
1076 | require "json" CRYSTAL Parsing and generating JSON documents
1077 | require "openssl" CRYSTAL OpenSSL integration
1078 | require "openssl/hmac" CRYSTAL Computing Hash-based Message Authentication Code (HMAC)
1079 | require "openssl/sha1" CRYSTAL OpenSSL SHA1 hash functions
1080 | require "yaml" CRYSTAL Serialization and deserialization of YAML 1.1
1081 | require "zlib" CRYSTAL Reading/writing Zlib compressed data as specified in RFC 1950
1082 |
1083 | ------- Supporting Functionality --------------------------------------------------------------------------
1084 | require "base64" CRYSTAL Encoding and decoding of binary data using base64 representation
1085 | require "bit_array" CRYSTAL Array data structure that compactly stores bits
1086 | require "callback" EXTERNAL Defining and invoking callbacks
1087 | require "compiled_license" EXTERNAL Compile in LICENSE files from project and dependencies
1088 | require "compiler/crystal/syntax/*" CRYSTAL Crystal syntax parser
1089 | require "crypto/bcrypt/password" CRYSTAL Generating, reading, and verifying Crypto::Bcrypt hashes
1090 | require "crypto/subtle" CRYSTAL
1091 | require "file_utils" CRYSTAL Supporting functions for files and directories
1092 | require "i18n" EXTERNAL Underlying I18N shard for Crystal
1093 | require "inflector" EXTERNAL Inflector for Crystal (a port of Ruby's ActiveSupport::Inflector)
1094 | require "process" CRYSTAL Supporting functions for working with system processes
1095 | require "random/secure" CRYSTAL Generating random numbers from a secure source provided by system
1096 | require "selenium" EXTERNAL Selenium Webdriver client
1097 | require "socket" CRYSTAL Supporting functions for working with sockets
1098 | require "socket/tcp_socket" CRYSTAL Supporting functions for TCP sockets
1099 | require "socket/unix_socket" CRYSTAL Supporting functions for UNIX sockets
1100 | require "string_inflection/kebab"EXTERNAL Singular/plurals words in "kebab" style ("foo-bar")
1101 | require "string_inflection/snake"EXTERNAL Singular/plurals words in "snake" style ("foo_bar")
1102 | require "uri" CRYSTAL Creating and parsing URI references as defined by RFC 3986
1103 | require "uuid" CRYSTAL Functions related to Universally unique identifiers (UUIDs)
1104 | require "weak_ref" CRYSTAL Weak Reference class allowing referenced objects to be GC-ed
1105 | require "zip" EXTERNAL Zip compression library, used for fetching zipped recipes
1106 |
1107 | require "ecr"
1108 | require "markd"
1109 | require "exception_page"
1110 | ```
1111 |
1112 |
1113 | Only the parts that are used end up in the compiled project.
1114 |
1115 | ## Environments
1116 |
1117 | After "[amber](https://github.com/amberframework/amber/blob/master/src/amber.cr)" shard is loaded, `Amber` module automatically includes [Amber::Environment](https://github.com/amberframework/amber/blob/master/src/amber/environment.cr) which adds the following methods:
1118 |
1119 | ```
1120 | Amber.settings # Singleton object, contains current settings
1121 | Amber.logger # Alias for Amber.settings.logger
1122 | Amber.env, Amber.env= # Env (environment) object (development, production, test)
1123 | ```
1124 |
1125 | The list of all available application settings is in [Amber::Environment::Settings](https://github.com/amberframework/amber/blob/master/src/amber/environment/settings.cr). These settings are loaded from the application's `config/environment/.yml` file and are then overriden by any settings in `config/application.cr`'s `Amber::Server.configure` block.
1126 |
1127 | [Env](https://github.com/amberframework/amber/blob/master/src/amber/environment/env.cr) (`amber.env`) also provides basic methods for querying the current environment:
1128 | ```crystal
1129 | def initialize(@env : String = ENV[AMBER_ENV]? || "development")
1130 | def in?(env_list : Array(EnvType))
1131 | def in?(*env_list : Object)
1132 | def to_s(io)
1133 | def ==(env2 : EnvType)
1134 |
1135 | ```
1136 |
1137 | ## Starting the Server
1138 |
1139 | It is important to explain exactly what happens from the time you run the application til Amber starts serving user requests:
1140 |
1141 | 1. `crystal src/.cr` - you or a script starts Amber
1142 | 1. `require "../config/*"` - as the first thing, `config/*` is required. Inclusion is in alphabetical order. Crystal only looks for *.cr files and only files in config/ are loaded (no subdirectories)
1143 | 1. `require "../config/application.cr"` - this is usually the first file in `config/`
1144 | 1. `require "./initializers/**"` - loads all initializers. There is only one initializer file by default, named `initializer/database.cr`. Here we have a double star ("**") meaning inclusion of all files including in subdirectories. Inclusion order is always "current directory first, then subdirectories"
1145 | 1. `require "amber"` - Amber itself is loaded
1146 | 1. Loading Amber makes `Amber::Server` class available
1147 | 1. `include Amber::Environment` - already in this stage, environment is determined and settings are loaded from yml file (e.g. from `config/environments/development.yml`. Settings are later available as `settings`
1148 | 1. `require "../src/controllers/application_controller"` - main controller is required. This is the base class for all other controllers
1149 | 1. It defines `ApplicationController`, includes JasperHelpers in it, and sets default layout ("application.slang").
1150 | 1. `require "../src/controllers/**"` - all other controllers are loaded
1151 | 1. `Amber::Server.configure` block is invoked to override any config settings
1152 | 1. `require "config/routes.cr"` - this again invokes `Amber::Server.configure` block, but concerns itself with routes and feeds all the routes in
1153 | 1. `Amber::Server.start` is invoked
1154 | 1. `instance.run` - implicitly creates a singleton instance of server, saves it to `@@instance`, and calls `run` on it
1155 | 1. Consults variable `settings.process_count`
1156 | 1. If process count is 1, `instance.start` is called
1157 | 1. If process count is > 1, the desired number of processes is forked, while main process enters sleep
1158 | 1. Forks invoke Process.run() and start completely separate, individual processes which go through the same initialization procedure from the beginning. Forked processes have env variable "FORKED" set to "1", and a variable "id" set to their process number. IDs are assigned in reverse order (highest number == first forked).
1159 | 1. `instance.start` is called for every process
1160 | 1. It saves current time and prints startup info
1161 | 1. `@handler.prepare_pipelines` is called. @handler is Amber::Pipe::Pipeline, a subclass of Crystal's [HTTP::Handler](https://crystal-lang.org/api/0.24.1/HTTP/Handler.html). `prepare_pipelines` is called to connect the pipes so the processing can work, and implicitly adds Amber::Pipe::Controller (the pipe in which app's controller is invoked) as the last pipe. This pipe's duty is to call Amber::Router::Context.process_request, which actually dispatches the request to the controller.
1162 | 1. `server = HTTP::Server.new(host, port, @handler)`- Crystal's HTTP server is created
1163 | 1. `server.tls = Amber::SSL.new(...).generate_tls if ssl_enabled?`
1164 | 1. Signal::INT is trapped (calls `server.close` when received)
1165 | 1. `loop do server.listen(settings.port_reuse) end` - server enters main loop
1166 |
1167 | ## Serving Requests
1168 |
1169 | Similarly as with starting the server, is important to explain exactly what is happening when Amber is serving requests:
1170 |
1171 | Amber's app serving model is based on Crystal's built-in, underlying functionality:
1172 |
1173 | 1. The server that is running is an instance of Crystal's
1174 | [HTTP::Server](https://crystal-lang.org/api/0.24.1/HTTP/Server.html)
1175 | 2. On every incoming request, a "handler" is invoked. As supported by Crystal, handler can be a simple Proc or an instance of [HTTP::Handler](https://crystal-lang.org/api/0.24.1/HTTP/Handler.html). HTTP::Handlers have a concept of "next" and multiple ones can be connected in a row. In Amber, these individual handlers are called "pipes" and currently at least two of them are always pre-defined — pipes named "Pipeline" and "Controller". The pipe "Pipeline" always executes first; it determines which pipeline the request is meant for and runs the first pipe in that pipeline. The pipe "Controller" always executes last; it consults the routing table, instantiates the appropriate controller, and invokes the appropriate method on it
1176 | 3. In the pipeline, every Pipe (Amber::Pipe::*, ultimately subclass of Crystal's [HTTP::Handler](https://crystal-lang.org/api/0.24.2/HTTP/Handler.html)) is invoked with one argument. That argument is
1177 | by convention called "context" and it is an instance of `HTTP::Server::Context`. By default it has two built-in methods — `request` and `response`, containing the request and response parts respectively. On top of that, Amber adds various other methods and variables, such as `router`, `flash`, `cookies`, `session`, `content`, `route`, `client_ip`, and others as seen in [router/context.cr](https://github.com/amberframework/amber/blob/master/src/amber/router/context.cr) and [extensions/http.cr](https://github.com/amberframework/amber/blob/master/src/amber/extensions/http.cr)
1178 | 4. Please note that calling the chain of pipes is not automatic; every pipe needs to call `call_next(context)` at the appropriate point in its execution to call the next pipe in a row. It is not necessary to check whether the next pipe exists, because currently `Amber::Pipe::Controller` is always implicitly added as the last pipe, so in the context of your pipes the next one always exists. State between pipes is not passed via separate variables but via modifying `context` and the data contained in it. Context persists for the duration of the request. Context persists for the duration of the request
1179 |
1180 | After that, pipelines, pipes, routes, and other Amber-specific parts come into play.
1181 |
1182 | So, in detail, from the beginning:
1183 |
1184 | 1. `loop do server.listen(settings.port_reuse) end` - main loop is running
1185 | 1. `spawn handle_client(server.accept?)` - handle_client() is called in a new fiber after connection is accepted
1186 | 1. `io = OpenSSL::SSL::Socket::Server.new(io, tls, sync_close: true) if @tls`
1187 | 1. `@processor.process(io, io)`
1188 | 1. `if request.is_a?(HTTP::Request::BadRequest); response.respond_with_error("Bad Request", 400)`
1189 | 1. `response.version = request.version`
1190 | 1. `response.headers["Connection"] = "keep-alive" if request.keep_alive?`
1191 | 1. `context = Context.new(request, response)` - this context is already extended by Amber with additional properties and methods
1192 | 1. `@handler.call(context)` - `Amber::Pipe::Pipeline.call()` is called
1193 | 1. `raise ...error... if context.invalid_route?` - route validity is checked early
1194 | 1. `if context.websocket?; context.process_websocket_request` - if websocket, parse as such
1195 | 1. `elsif ...; ...pipeline.first...call(context)` - if regular HTTP request, call the first handler in the appropriate pipeline
1196 | 1. `call_next(context)` - each pipe calls call_next(context) somewhere during its execution, and all pipes are executed
1197 | 1. `context.process_request` - the always-last pipe (Amber::Pipe::Controller) calls `process_request` to dispatch the action to controller. After that last pipe, the stack of call_next()s is "unwound" back to the starting position
1198 | 1. `context.finalize_response` - minor final adjustments to response are made (headers are added, and response body is printed unless action was HEAD)
1199 |
1200 | ## Support Routines
1201 |
1202 | In [support/](https://github.com/amberframework/amber/tree/master/src/amber/support) directory there is a number of various support files that provide additional, ready-made routines.
1203 |
1204 | Currently, the following can be found there:
1205 |
1206 | ```
1207 | client_reload.cr - Support for reloading developer's browser
1208 |
1209 | file_encryptor.cr - Support for storing/reading encrypted versions of files
1210 | message_encryptor.cr
1211 | message_verifier.cr
1212 |
1213 | locale_formats.cr - Very basic locate data for various manually-added locales
1214 |
1215 | mime_types.cr - List of MIME types and helper methods for working with them:
1216 |
1217 | def self.mime_type(format, fallback = DEFAULT_MIME_TYPE)
1218 | def self.zip_types(path)
1219 | def self.format(accepts)
1220 | def self.default
1221 | def self.get_request_format(request)
1222 | ```
1223 |
1224 | ## Amber behind a Load Balancer | Reverse Proxy | ADC
1225 |
1226 | (In this section, the terms "Load Balancer", "Reverse Proxy", "Proxy", and "Application Delivery Controller" (ADC) are used interchangeably.)
1227 |
1228 | By default, in development environment Amber listens on port 3000, and in production environment it listens on port 8080. This makes it very easy to run a load balancer on ports 80 (HTTP) and 443 (HTTPS) and proxy user requests to Amber.
1229 |
1230 | There are three groups of benefits of running Amber behind a proxy:
1231 |
1232 | On a basic level, a proxy will perform TCP and HTTP normalization — it will filter out invalid TCP packets, flags, window sizes, sequence numbers, and SYN floods. It will only pass valid HTTP requests through (protecting the application from protocol-based attacks) and smoothen out deviations which are tolerated by HTTP specification (such as multi-line HTTP headers). Finally, it will provide HTTP/2 support for your application and perform SSL and compression offloading so that these functions are done on the load balancers rather than on the application servers.
1233 |
1234 | Also, as an important implementation-specific detail, Crystal currently does not provide applications with the information on the client IPs that are making HTTP requests. Therefore, Amber is by default unaware of them. With a proxy in front of Amber and using Amber's pipe `ClientIp`, the client IP information will be passed from the proxy to Amber and be available as `context.client_ip.address`.
1235 |
1236 | On an intermediate level, a proxy will provide you with caching and scaling and serve as a versatile TCP and HTTP load balancer. It will cache static files, route your application and database traffic to multiple backend servers, balance multiple protocols based on any criteria, fix and rewrite HTTP traffic, and so on. The benefits of starting application development with acceleration and scaling in mind from the get-go are numerous.
1237 |
1238 | On an advanced level, a proxy will allow you to keep track of arbitrary statistics and counters, perform GeoIP offloading and rate limiting, filter out bots and suspicious web clients, implement DDoS protection and web application firewall, troubleshoot network conditions, and so on.
1239 |
1240 | [HAProxy](www.haproxy.org) is an excellent proxy to use and to run it you will only need the `haproxy` binary, two command line options, and a config file. A simple HAProxy config file that can be used out of the box is available in [support/haproxy.conf](https://github.com/docelic/amber-introduction/blob/master/support/haproxy.conf). This config file will be expanded over time into a full-featured configuration to demonstrate all of the above-mentioned points, but even by default the configuration should be good enough to get you started with practical results.
1241 |
1242 | HAProxy comes pre-packaged for most GNU/Linux distributions and MacOS, but if you do not see version 1.8.x available, it is recommended to manually install the latest stable version.
1243 |
1244 | To compile the latest stable HAProxy from source, you could use the following procedure:
1245 |
1246 | ```
1247 | git clone http://git.haproxy.org/git/haproxy-1.8.git/
1248 | cd haproxy-1.8
1249 | make -j4 TARGET=linux2628 USE_OPENSSL=1
1250 | ```
1251 |
1252 | The compilation will go trouble-free and you will end up with the binary named `haproxy` in the current directory.
1253 |
1254 | To obtain the config file and set up the basic directory structure, please run the following in your Amber app directory:
1255 |
1256 | ```sh
1257 | cd config
1258 | wget https://raw.githubusercontent.com/docelic/amber-introduction/master/support/haproxy.conf
1259 | cd ..
1260 | mkdir -p var/{run,empty}
1261 | ```
1262 |
1263 | And finally, to start HAProxy in development/foreground mode, please run:
1264 |
1265 | ```sh
1266 | sudo ../haproxy-1.8/haproxy -f config/haproxy.conf -d
1267 | ```
1268 |
1269 | And then start `amber watch` and point your browser to [http://localhost/](http://localhost/) instead of [http://localhost:3000/](http://localhost:3000/)!
1270 |
1271 | Please also note that this HAProxy configuration enables the built-in HAProxy status page at [http://localhost/server-status](http://localhost/server-status) and restricts access to it to localhost.
1272 |
1273 | When you confirm everything is working, you can omit the `-d` flag and it will start HAProxy in background, returning the shell back to you. You can then forget about HAProxy until you modify its configuration and want to reload it. Then simply call `kill -USR2 var/run/haproxy.pid`.
1274 |
1275 | Finally, now that we are behind a proxy, to get access to client IPs we can enable the following line in `config/routes.cr`:
1276 |
1277 | ```
1278 | plug Amber::Pipe::ClientIp.new(["X-Forwarded-For"])
1279 | ```
1280 |
1281 | And we can modify one of the views to display the user IP address. Assuming you are using slang, you could edit the default view file `src/views/home/index.slang` and add the following to the bottom to confirm the new behavior:
1282 |
1283 | ```
1284 | a.list-group-item.list-group-item-action href="#" = "IP Address: " + ((ip = context.client_ip) ? ip.address : "Unknown")
1285 | ```
1286 |
--------------------------------------------------------------------------------
/support/Makefile:
--------------------------------------------------------------------------------
1 | all: amber app diffs ../README.md
2 |
3 | readme_SOURCES=$(shell find -maxdepth 1 -type f)
4 |
5 | amber:
6 | git clone https://github.com/amberframework/amber
7 | cd amber && shards
8 | cd amber && make
9 |
10 | app: amber
11 | amber/bin/amber new app
12 | cd app && shards
13 |
14 | .PHONY: diffs
15 | diffs:
16 | bash run-diffs.bash
17 |
18 | ../README.md: $(readme_SOURCES)
19 | perl tpl2md.pl
20 |
21 | clean:
22 | rm -rf amber app
23 |
--------------------------------------------------------------------------------
/support/README.md.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Introduction to the Amber Web Framework
4 | And its Out-of-the-Box Features
5 |
6 |
7 |
8 | Amber makes building web applications easy, fast, and enjoyable.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{{TOC}}}
17 |
18 | # Introduction
19 |
20 | **Amber** is a web application framework written in [Crystal](http://www.crystal-lang.org). Homepage can be found at [amberframework.org](https://amberframework.org/), docs at [Amber Docs](https://docs.amberframework.org), GitHub repository at [amberframework/amber](https://github.com/amberframework/amber), and the chat on [Gitter](https://gitter.im/amberframework/amber) or on the FreeNode IRC channel #amber.
21 |
22 | Amber is inspired by Kemal, Rails, Phoenix, and other frameworks. It is simple to get used to, and much more intuitive than frameworks like Rails. (But it does inherit many concepts from Rails that are good.)
23 |
24 | This document is here to describe everything that Amber offers out of the box, sorted in a logical order and easy to consult repeatedly over time. The Crystal level is not described; it is expected that the readers coming here have a formed understanding of [Crystal and its features](https://crystal-lang.org/docs/overview/).
25 |
26 | # Installation
27 |
28 | ```shell
29 | git clone https://github.com/amberframework/amber
30 | cd amber
31 | make # The result of 'make' will be one file -- command line tool bin/amber
32 |
33 | #: To install the file, or to symlink the system-wide executable to current directory, run one of:
34 | make install # default PREFIX is /usr/local
35 | make install PREFIX=/usr/local/stow/amber
36 | make force_link # can also specify PREFIX=...
37 | ```
38 |
39 | ("stow" mentioned above is referring to [GNU Stow](https://www.gnu.org/software/stow/).)
40 |
41 | After installation or linking, `amber` is the command you will be using for creating and managing Amber apps.
42 |
43 | Please note that some users prefer (or must use for compatibility reasons) local Amber executables which match the version of Amber used in their project. For that, each Amber project's `shard.yml` ships with the build target named "amber":
44 |
45 | ```
46 | targets:
47 | ...
48 | amber:
49 | main: lib/amber/src/amber/cli.cr
50 |
51 | ```
52 |
53 | Thanks to it, running `shards build amber` will compile local Amber found in `lib/amber/` and place the executable into the project's local file `bin/amber`.
54 |
55 | # Creating New Amber App
56 |
57 | ```shell
58 | amber new [-d DATABASE] [-t TEMPLATE_LANG] [-m ORM_MODEL] [--deps]
59 | ```
60 |
61 | Supported databases are [PostgreSQL](https://www.postgresql.org/) (pg, default), [MySQL](https://www.mysql.com/) (mysql), and [SQLite](https://sqlite.org/) (sqlite).
62 |
63 | Supported template languages are [slang](https://github.com/jeromegn/slang) (default) and [ecr](https://crystal-lang.org/api/0.21.1/ECR.html). (But any languages can be used; more on that can be found below in [Template Languages](#template_languages).)
64 |
65 | Slang is extremely elegant, but very different from the traditional perception of HTML.
66 | ECR is HTML-like, very similar to Ruby ERB, and also much less efficient than slang, but it may be the best choice for your application if you intend to use some HTML site template (e.g. from [themeforest](https://themeforest.net/)) whose pages are in HTML + CSS or SCSS. (Or you could also try [html2slang](https://github.com/docelic/html2slang/) which converts the bulk of HTML pages into slang with relatively good accuracy.)
67 |
68 | Supported ORM models are [granite](https://github.com/amberframework/granite-orm) (default) and [crecto](https://github.com/Crecto/crecto).
69 |
70 | Granite is Amber's native, nice, and effective ORM model where you mostly write your own SQL. For example, all search queries typically look like `YourModel.all("WHERE field1 = ? AND field2 = ?", [value1, value2])`. But it also has belongs/has relations, and some other little things.
71 |
72 | Supported migrations engine is [micrate](https://github.com/amberframework/micrate). (But any migrations engines can be used; more on that can be found below in [Custom Migrations Engine](#custom_migrations_engine).)
73 |
74 | Micrate is very simple and you basically write raw SQL in your migrations. There are just two keywords in the migration files which give instructions whether the SQLs that follow pertain to migrating up or down. These keywords are "-- +micrate Up" and "-- +micrate Down". If you have complex SQL statements that contain semicolons then you also enclose each in "-- +micrate StatementBegin" and "-- +micrate StatementEnd".
75 |
76 | Finally, if argument `--deps` is provided, Amber will automatically run `shards` in the new project's directory after creation to download the shards required by the project.
77 |
78 | Please note that shards-related commands use the directory `.shards/` as local staging area before the contents are fully ready to replace shards in `lib/`.
79 |
80 | # Running the App
81 |
82 | Before building or running Amber applications, you should install the following system packages: `libevent-dev libgc-dev libxml2-dev libssl-dev libyaml-dev libcrypto++-dev libsqlite3-dev`. These packages will make sure that you do not run into missing header files as soon as you try to run the application.
83 |
84 | Other than that, the app can be started as soon as you have created it and ran `shards` in the app directory.
85 | (It is not necessary to run `shards` if you have invoked `amber new` with the argument `--deps`; in that case Amber did it for you.)
86 |
87 | Please note that the application is always compiled, regardless of whether one is using the Crystal command 'run' (the default) or 'build'. It is just that in run mode, the resulting binary is typically compiled without optimizations (to improve build speed) and is not saved to a file, but is compiled, executed, and then discarded.
88 |
89 | To run the app, you could use a couple different approaches:
90 |
91 | ```shell
92 | #: For development, clean and simple - compiles and runs your app:
93 | crystal src/.cr
94 |
95 | #: Compiles and runs app in 'production' environment:
96 | AMBER_ENV=production crystal src/.cr
97 |
98 | #: For development, clean and simple - compiles and runs your app, but
99 | #: also watches for changes in files and rebuilds/re-runs automatically:
100 | amber watch
101 | ```
102 |
103 | Amber apps by default use a feature called "port reuse" available in newer Linux kernels. If you get an error "setsockopt: Protocol not available" upon running the app, it means your kernel does not support it. Please edit `config/environments/development.yml` and set "port_reuse" to false.
104 |
105 | # Building the App and Build Troubleshooting
106 |
107 | To build the application in a simple and effective way, you would run the following to produce executable file `bin/`:
108 |
109 | ```shell
110 | #: For production, compiles app with optimizations and places it in bin/.
111 | shards build --production
112 | ```
113 |
114 | To build the application in a more manual way, skip dependency checking, and control more of the options, you would run:
115 |
116 | ```shell
117 | #: For production, compiles app with optimizations and places it in bin/.
118 | #: Crystal by default compiles using 8 threads (tune if needed with --threads NUM)
119 | crystal build --no-debug --release --verbose -t -s -p -o bin/ src/.cr
120 | ```
121 |
122 | As mentioned, for faster build speed, development versions are compiled without the `--release` flag. With the `--release` flag the compilation takes noticeably longer, but the resulting binary has incredible performance.
123 |
124 | Thanks to Crystal's compiler implementation, only the parts actually used are added to the executable. Listing dependencies in `shard.yml` or even using `require`s in your program will generally not affect what is compiled in.
125 |
126 | Crystal caches partial results of the compilation (*.o files etc.) under `~/.cache/crystal/` for faster subsequent builds. This directory is also where temporary binaries are placed when one runs programs with `crystal [run]` rather than `crystal build`.
127 |
128 | Sometimes building the app will fail on the C level because of missing header files or libraries. If Crystal doesn't print the actual C error, it will at least print the compiler line that caused it.
129 |
130 | The best way to see the actual error from there is to copy-paste the command printed and run it manually in the terminal. The error will be shown and from there the cause and solution will be determined easily. Usually some library or header files will be missing, such as those mentioned above in [Running the App](#running_the_app).
131 |
132 | There are some issues with the `libgc` library here and there. Sometimes it helps to reinstall the system's package `libgc-dev`.
133 |
134 | # REPL
135 |
136 | Often times, it is very useful to enter an interactive console (think of IRB shell) with all application classes initialized etc. In Ruby this would be done with IRB or with a command like `rails console`.
137 |
138 | Crystal does not have a free-form [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop), but you can save and execute scripts in the context of the application. One way to do it is via command `amber x [filename]`. This command will allow you to type or edit the contents, and then execute the script.
139 |
140 | Another, possibly more flexible way to do it is via standalone REPL-like tools [cry](https://github.com/elorest/cry) or [icr](https://github.com/crystal-community/icr). `cry` began as an experiment and a predecessor to `amber x`, but now offers additional functionality such as repeatedly editing and running the script if `cry -r` is invoked.
141 |
142 | In any case, running a script "in application context" simply means requiring `config/application.cr` (and through it, `config/**`). Therefore, be sure to list all your requires in `config/application.cr` so that everything works as expected, and if you are using `cry` or `icr`, have `require "./config/application"` as the first line.
143 |
144 | # File Structure
145 |
146 | So, at this point you might be wanting to know what's placed where in an Amber application. The default structure looks like this:
147 |
148 | ```
149 | ./config/ - All configuration, detailed in subsequent lines:
150 | ./config/initializers/ - Initializers (code you want executed at the very beginning)
151 | ./config/environments/ - Environment-specific YAML configurations (development, production, test)
152 | ./config/application.cr - Main configuration file for the app. Generally not touched (apart
153 | from adding "require"s to the top) because most of the config
154 | settings are specified in YAML files in config/environments/
155 | ./config/webpack/ - Webpack (asset bundler) configuration
156 | ./config/routes.cr - All routes
157 |
158 | ./db/migrations/ - All DB migration files (created with "amber g migration ...")
159 |
160 | ./public/ - The "public" directory for static files
161 | ./public/dist/ - Directory inside "public" for generated files and bundles
162 | ./public/dist/images/
163 |
164 | ./src/ - Main source directory, with .cr being the main file
165 | ./src/controllers/ - All controllers
166 | ./src/models/ - All models
167 | ./src/views/layouts/ - All layouts
168 | ./src/views/ - All views
169 | ./src/views/home/ - Views for HomeController (the app's "/" path)
170 | ./src/locales/ - Toplevel directory for locale (translation) files named [lang].yml
171 | ./src/assets/ - Static assets which will be bundled and placed into ./public/dist/
172 | ./src/assets/fonts/
173 | ./src/assets/images/
174 | ./src/assets/javascripts/
175 | ./src/assets/stylesheets/
176 |
177 | ./spec/ - Toplevel directory for test files named "*_spec.cr"
178 | ```
179 |
180 | I prefer to have some of these directories accessible directly in the root directory of the application and to have the config directory aliased to `etc`, so I run:
181 |
182 | ```
183 | [[[cat ln-sfs]]]
184 | ```
185 |
186 | # Database Commands
187 |
188 | Amber provides a group of subcommands under `amber db` to allow working with the database. The simple commands you will most probably want to run first just to see things working are:
189 |
190 | ```shell
191 | amber db create
192 | amber db status
193 | amber db version
194 | ```
195 |
196 | Before these commands will work, you will need to configure database
197 | credentials as follows:
198 |
199 | First, create a user to access the database. For PostgreSQL, this is done by invoking something like:
200 |
201 | ```shell
202 | $ sudo su - postgres
203 | $ createuser -dElPRS myuser
204 | Enter password for new role:
205 | Enter it again:
206 | ```
207 |
208 | Then, edit `config/environments/development.yml` and configure "database_url:" to match your settings. If nothing else, the part that says "postgres:@" should be replaced with "yourusername:yourpassword@".
209 |
210 | And then try the database commands from the beginning of this section.
211 |
212 | Please note that for the database connection to succeed, everything must be set up correctly — hostname, port, username, password, and database name must be valid, the database server must be accessible, and the database must actually exist unless you are invoking `amber db create` to create it. In case of *any error in any of these requirements*, the error message will be terse and just say "Connection unsuccessful: ". The solution is simple, though - simply use the printed database_url to manually attempt a connection to the database with the same parameters, and the problem will most likely quickly reveal itself.
213 |
214 | (If you are sure that the username and password are correct and that the database server is accessible, then the most common problem is that the database does not exist yet, so you should run `amber db create` as the first command to create it.)
215 |
216 | Please note that the environment files for non-production environment are given in plain text. Environment file for the production environment is encrypted for additional security and can be seen or edited by invoking `amber encrypt`.
217 |
218 | # Pipes and Pipelines
219 |
220 | In very simple frameworks it could suffice to directly map incoming requests to methods in the application, call them, and return their output to the user.
221 |
222 | More elaborate application frameworks like Amber provide many more features and flexibility, and allow pluggable components to be inserted and executed in the chosen order before the actual controller method is invoked to handle the request.
223 |
224 | These components are in general terminology called "middleware". Crystal calls them "handlers", and Amber calls them "pipes". In any case, in Amber applications they all refer to the same thing — classes that `include` Crystal's module [HTTP::Handler](https://crystal-lang.org/api/0.24.2/HTTP/Handler.html) and that implement method `def call(context)`. (So in Amber, this functionality is based on Crystal's HTTP server's built-in support for handlers/pipes.)
225 |
226 | Pipes work in such a way that invoking the pipes is not automatic, but each pipe must explicitly invoke `call_next(context)` to call the next pipe in a row. This is actually desirable because it makes it possible to call the next pipe at exactly the right place in the code where you want it and if you want it — at the beginning, in the middle, or at the end of your current pipe's code, or not at all.
227 |
228 | The request and response data that pipes need in order to run and do anything meaningful is passed as the first argument to every pipe, and is by convention named "context".
229 |
230 | Context persists for the duration of the request and is the place where data that should be shared/carried between pipes should be saved. Amber extends the default [HTTP::Server::Context](https://crystal-lang.org/api/0.24.2/HTTP/Server/Context.html) class with many additional fields and methods as can be seen in [router/context.cr](https://github.com/amberframework/amber/blob/master/src/amber/router/context.cr) and [extensions/http.cr](https://github.com/amberframework/amber/blob/master/src/amber/extensions/http.cr).
231 |
232 | Handlers or pipes are not limited in what they can do. It is normal that they sometimes stop execution and return an error, or fulfil the request on their own without even passing the request through to the controller. Examples of such pipes are [CSRF](https://github.com/amberframework/amber/blob/master/src/amber/pipes/csrf.cr) which stops execution if CSRF token is incorrect, or [Static](https://github.com/amberframework/amber/blob/master/src/amber/pipes/static.cr) which autonomously handles delivery of static files.
233 |
234 | Using pipes promotes code reuse and is a nice way to plug various standard or custom functionality in the request serving process without requiring developers to duplicate code or include certain parts of code in every controller action.
235 |
236 | Additionally, in Amber there exists a concept of "pipelines". Pipelines are logical groups of pipes. The discussion about them continues in the next section.
237 |
238 | # Routes, Controller Methods, and Responses
239 |
240 | Before expanding the information on pipes and pipelines, let's explain the concept of routes.
241 |
242 | Routes connect incoming requests (HTTP methods and URL paths) to specific controllers and controller methods in your application. Routes are checked in the order they are defined and the first route that matches wins.
243 |
244 | All routes belong to a certain pipeline (like "web", "api", or similar). When a route matches, Amber simply executes all pipes in the pipeline under which that route has been defined. The last pipe in every pipeline is implicitly the pipe named "[Controller](https://github.com/amberframework/amber/blob/master/src/amber/pipes/controller.cr)". That's the pipe which actually looks into the original route, instantiates the specified controller, and calls the specified method in it. Please note that this is currently non-configurable — the controller pipe is always automatically added as the last pipe in the pipeline and it is executed unless processing stops in one of the earlier pipes.
245 |
246 | The configuration for pipes, pipelines, and routes is found in the file `config/routes.cr`. This file invokes the same `configure` block that `config/application.cr` does, but since routes configuration is important and can also be lengthy and complex, Amber keeps it in a separate file.
247 |
248 | Amber includes commands `amber routes` and `amber pipelines` to display route and pipeline configurations. By default, the output for routes looks like the following:
249 |
250 | ```shell
251 | $ amber routes
252 |
253 | [[[cd app && ../amber/bin/amber routes --no-color]]]
254 | ```
255 |
256 | From the first line of the output we see that a "GET /" request will cause all pipes in the pipeline "web" to be executed, and then
257 | `HomeController.new.index` method will be called.
258 |
259 | In the `config/routes.cr` code, this is simply achieved with the line:
260 |
261 | ```crystal
262 | routes :web do
263 | get "/", HomeController, :index
264 | end
265 | ```
266 |
267 | The return value of the controller method is returned as response body to the client.
268 |
269 | As another example, the following definition would cause a POST request to "/registration" to result in invoking `RegistrationController.new.create`:
270 |
271 | ```
272 | post "/registration", RegistrationController, :create
273 | ```
274 |
275 | By convention, standard HTTP verbs (GET/HEAD, POST, PUT/PATCH, and DELETE) should be routed to standard-named methods on the controllers — `show`, `create`, `update`, and `destroy`. However, there is nothing preventing you from routing URLs to any methods you want in the controllers, such as we've seen with `index` above.
276 |
277 | Websocket routes are supported too.
278 |
279 | The DSL language specific to `config/routes.cr` file is defined in [dsl/router.cr](https://github.com/amberframework/amber/blob/master/src/amber/dsl/router.cr) and [dsl/server.cr](https://github.com/amberframework/amber/blob/master/src/amber/dsl/server.cr).
280 |
281 | It gives you the following top-level commands/blocks:
282 |
283 | ```
284 | #: Define a pipeline
285 | pipeline :name do
286 | # ... list of pipes ...
287 | end
288 |
289 | #: Group a set of routes
290 | routes :pipeline_name, "/optional_path_prefix" do
291 | # ... list of routes ...
292 | end
293 | ```
294 |
295 | This is used in practice in the following way in `config/routes.cr`:
296 |
297 | ```crystal
298 | Amber::Server.configure do |app|
299 | pipeline :web do
300 | # Plug is the method used to connect a pipe (middleware).
301 | # A plug accepts an instance of HTTP::Handler.
302 | plug Amber::Pipe::Logger.new
303 | end
304 |
305 | routes :web do
306 | get "/", HomeController, :index # Routes "GET /" to HomeController.new.index
307 | post "/test", PageController, :test # Routes "POST /test" to PageController.new.test
308 | end
309 | end
310 | ```
311 |
312 | Within "routes" blocks the following commands are available:
313 |
314 | ```crystal
315 | get, post, put (or patch), delete, options, head, trace, connect, websocket, resources
316 | ```
317 |
318 | Most of these actions correspond to the respective HTTP methods; `websocket` defines websocket routes; and `resources` is a macro defined as:
319 |
320 | ```crystal
321 | macro resources(resource, controller, only = nil, except = nil)
322 | ```
323 |
324 | Unless `resources` is confined with arguments `only` or `except`, it will automatically route `get`, `post`, `put/patch`, and `delete` HTTP methods to methods `index`, `show`, `new`, `edit`, `create`, `update`, and `destroy` on the controller.
325 |
326 | Please note that it is not currently possible to define a different behavior for GET and HEAD HTTP methods on the same path. If a GET is defined, it will also automatically add the matching HEAD route. Specifying HEAD route manually would then result in two HEAD routes existing for the same path and trigger `Amber::Exceptions::DuplicateRouteError`.
327 |
328 | # Views
329 |
330 | Information about views can be summarized in the following bullet points:
331 |
332 | - Views in an Amber project are located under the toplevel directory `src/views/`
333 | - Views are typically rendered using `render()`
334 | - The first argument given to `render()` is the template name (e.g. `render("index.slang")`)
335 | - `render("index.slang")` will look for a view named `src/views//index.slang`
336 | - `render("./abs/or/rel/path.slang")` will look for a template in that specific path
337 | - There is no unnecessary magic applied to template names — names specified are the names that will be looked up on disk
338 | - If you are not rendering a partial, by default the template will be wrapped in a layout
339 | - If the layout name isn't specified, the default layout will be `views/layouts/application.slang`
340 | - To render a partial, use `render( partial: "_name.ext")`
341 | - Partials begin with "\_" by convention, but that is not required. If they are named with "\_", then the "\_" must be mentioned as part of the name
342 | - Templates are read from disk and compiled into the application at compile time. This makes them fast to access and also read-only which is a useful side-benefit
343 |
344 | The `render` macro is usually invoked at the end of the controller method. This makes its return value be the return value of the controller method as a whole, and as already mentioned, the controller method's return value is returned to the client as response body.
345 |
346 | It is also important to know that `render` is a macro and that views are rendered directly (in-place) as part of the controller method.
347 | This results in a very interesting property — since `render` executes directly in the controller method, it sees all local variables in it and view data does not have to be passed via instance variables. This particular aspect is explained in more detail further below under [Variables in Views](#variables_in_views).
348 |
349 | ## Template Languages
350 |
351 | In the introduction we've mentioned that Amber supports two template languages — [slang](https://github.com/jeromegn/slang) (default) and [ecr](https://crystal-lang.org/api/0.21.1/ECR.html).
352 |
353 | That's because Amber ships with a minimal working layout (a total of 3 files) in those languages, but there is nothing preventing you from using any other languages if you have your own templates or want to convert existing ones.
354 |
355 | Amber's default rendering engine is [Kilt](https://github.com/jeromegn/kilt), so all languages supported by Kilt should be usable out of the box. Amber does not make assumptions about the template language used; the view file's extension will determine which parser will be invoked (e.g. ".ecr" for ecr, ".slang" for slang).
356 |
357 | ### Liquid Template Language
358 |
359 | The original [Kilt](https://github.com/jeromegn/kilt) repository now has support for the Liquid template language.
360 |
361 | Please note, however, that Liquid as a template language comes with non-typical requirements — primarily, it requires a separate store ("context") for user data which is to be available in templates, and also it does not allow arbitrary functions, objects, object methods, and data types to be used in its templates.
362 |
363 | As such, Amber's principle of rendering the templates directly inside controller methods (and thus making all local variables automatically available in views) cannot be used because Liquid's context is separate and local variables are not there.
364 |
365 | Also, Liquid's implementation by default tries to be helpful and it automatically creates a new context. It copies all instance variables (@ivars) from the current object into the newly created context, which again cannot be used with Amber for two reasons.
366 | First, because the copying does not work for data other than basic types (e.g. saying `@process = Process` does not make `{{ process.pid }}` usable in a Liquid template). Second, because Amber's controllers already contain various instance variables that should not or can not be serialized, so even simply saying `render("index.liquid")` would result in a compile-time error in Crystal even if the template itself was empty.
367 |
368 | Also, Amber's `render` macro does not accept extra arguments, so a custom context can't be passed to Kilt and from there to Liquid.
369 |
370 | Therefore, the best approach to work with Liquid in Amber is to create a custom context, populate it with desired values, and then invoke `Kilt.render` macro directly (without using Amber's `render` macro). The pull request [#610](https://github.com/amberframework/amber/pull/610) to make rendering engines includable/choosable at will was refused by the Amber project, so if you are bothered that the default `render` macro is present in your application even though you do not use it, simply comment the line `include Helpers::Render` in Amber's [controller/base.cr](https://github.com/amberframework/amber/blob/master/src/amber/controller/base.cr).
371 |
372 | Please also keep in mind not to use the name "context" for the variable that will hold Liquid's context, because that would take precedence over the `context` getter that already exists on the controllers and is used to access `HTTP::Server::Context` object.
373 |
374 | So, altogether, a working example for rendering Liquid templates in Amber would look like the following (showing the complete controller code for clarity):
375 |
376 | ```
377 | class HomeController < ApplicationController
378 | def index
379 | ctx = Liquid::Context.new
380 | ctx.set "process", { "pid" => Process.pid }
381 |
382 | # The following would render src/views/[controller]/index.liquid
383 | Kilt.render "index.liquid", ctx
384 |
385 | # The following would render specified path, relative to app base directory
386 | Kilt.render "src/views/myview.liquid", ctx
387 | end
388 | end
389 | ```
390 |
391 | # Logging
392 |
393 | Amber logger (based on standard Crystal's class `Logger`) is initialized as soon as `require "amber"` is called, as part of reading the settings and initializing the environment.
394 |
395 | The variable containing the logger is `Amber.settings.logger`. For convenience, it is also available as `Amber.logger`. In the context of a Controller, it is also available as simply `logger`.
396 |
397 | Controllers and views execute in the same class (the class of the controller), so calling the following anywhere in the controller or views will produce the expected log line:
398 |
399 | ```crystal
400 | logger.info "Informational Message"
401 | ```
402 |
403 | Log levels available are `debug`, `info`, `warn`, `error`, `fatal`, and `unknown`.
404 |
405 | The second, optional parameter passed to the log method will affect the displayed name of the subsystem from which the message originated. For example:
406 |
407 |
408 | ```crystal
409 | logger.warn "Starting up", "MySystem"
410 | ```
411 |
412 | would result in the log line:
413 |
414 | ```
415 | 03:17:04 MySystem | (WARN) Starting up
416 | ```
417 |
418 | In you still need a customized logger for special cases or purposes, please create a separate `Logger.new` yourself.
419 |
420 | # Parameter Validation
421 |
422 | First of all, Amber framework considers query and body params equal and makes them available to the application in the same, uniform way.
423 |
424 | Second of all, the params handling in Amber is not programmed in a truly clean and non-overlapping way, but the description here should be clear to understand.
425 |
426 | There are just three important methods to have in mind — `params.validation {...}` which defines validation rules, `params.valid?` which returns whether parameters pass validation, and `params.validate!` which requires that parameters pass validation or raises an error.
427 |
428 | A simple validation process in a controller could look like this (showing the whole Controller class for completeness):
429 |
430 | ```crystal
431 | class HomeController < ApplicationController
432 | def index
433 | params.validation do
434 | required(:name) { |n| n.size > 6 } # Name must have at least 6 characters
435 | optional(:phone) { |n| n.phone? } # Phone must look like a phone number
436 | end
437 |
438 | "Params valid: #{params.valid?.to_s}
Name is: #{params[:name]}"
439 | end
440 | end
441 | ```
442 |
443 | (Extensions to the String class such as `phone?` seen above come especially handy for writing validations. Please see [Extensions](#extensions) below for the complete list of built-in extensions available.)
444 |
445 | With this foundation in place, let's take a step back to explain the underlying principles and also expand the full description:
446 |
447 | As already mentioned above, for every incoming request, Amber uses data from `config/routes.cr` to determine which controller and method in it should handle the request. Then it instantiates that controller (calls `new` on it), and because all controllers inherit from `ApplicationController` (which in turn inherits from `Amber::Controller:Base`), the following code is executed as part of initialize:
448 |
449 | ```crystal
450 | protected getter params : Amber::Validators::Params
451 |
452 | def initialize(@context : HTTP::Server::Context)
453 | @params = Amber::Validators::Params.new(context.params)
454 | end
455 | ```
456 |
457 | In other words, `params` object is initialized using raw params passed with the user's request (i.e. `context.params`). From there, it is important to know that `params` object contains 4 important variables (getters):
458 |
459 | 1. `params.raw_params` - this is a reference to hash `context.params` created during initialize, and all methods invoked on `params` directly (such as `[]`, `[]?`, `[]=`, `add`, `delete`, `each`, `fetch`, etc.) are forwarded to this object. Please note that this is a reference and not a copy, so all modifications made there also affect `context.params`
460 | 1. `params.rules` - this is initially an empty list of validation rules. It is filled in as validation rules are defined using `params.validation {...}`
461 | 1. `params.params` - this is a hash of key=value parameters, but only those that were mentioned in the validation rules and that passed validation when `valid?` or `validate!` were called. This list is re-initialized on every call to `valid?` or `validate!`. Using this hash ensures that you only work with validated/valid parameters
462 | 1. `params.errors` - this is a list of all eventual errors that have ocurred during validation with `valid?` or `validate!`. This list is re-initialized on every call to `valid?` or `validate!`
463 |
464 | And this is basically all there is to it. From here you should have a complete understanding how to work with params validation in Amber.
465 |
466 | (TODO: Add info on model validations)
467 |
468 | # Static Pages
469 |
470 | It can be pretty much expected that a website will need a set of simple, "static" pages. Those pages are served by the application, but mostly don't use a database nor any complex code. Such pages might include About and Contact pages, Terms amd Conditions, and so on. Making this work is trivial and will serve as a great example.
471 |
472 | Let's say that, for simplicity and logical grouping, we want all "static" pages to be served by a controller we will create, named "PageController". We will group all these "static" pages under a common web-accessible prefix of /page/, and finally we will route page requests to PageController's methods. Because these pages won't be backed by objects, we won't need models or anything else other than one controller method and one view per each page.
473 |
474 | Let's create the controller:
475 |
476 | ```shell
477 | amber g controller page
478 | ```
479 |
480 | Then, we edit `config/routes.cr` to link e.g. URL "/page/about" to method about() in PageController. We do this inside the "routes :web" block:
481 |
482 | ```
483 | routes :web, "/page" do
484 | ...
485 | get "/about", PageController, :about
486 | ...
487 | end
488 | ```
489 |
490 | Then, we edit the controller and actually add method about(). This method can just directly return a string in response, or it can render a view. In any case, the return value from the method will be returned as the response body to the client, as usual.
491 |
492 | ```shell
493 | $ vi src/controllers/page_controller.cr
494 |
495 | #: Inside the file, we add:
496 |
497 | def about
498 | # "return" can be omitted here. It is included for clarity.
499 | render "about.ecr"
500 | end
501 | ```
502 |
503 | Since this is happening in the "page" controller, the view directory for finding the templates will default to `src/views/page/`. We will create the directory and the file "about.ecr" in it:
504 |
505 | ```shell
506 | $ mkdir -p src/views/page/
507 | $ vi src/views/page/about.ecr
508 |
509 | #: Inside the file, we add:
510 |
511 | Hello, World!
512 | ```
513 |
514 | Because we have called render() without additional arguments, the template will default to being rendered within the default application layout, `views/layouts/application.cr`.
515 |
516 | And that is it! The request for `/page/about` will reach the router, the router will invoke `PageController.new.about()`, that method will render template `src/views/page/about.ecr` in the context of layout `views/layouts/application.cr`, the result of rendering will be a full page with content `Hello, World!` in the body, and that result will be returned to the client as response body.
517 |
518 | # Variables in Views
519 |
520 | As mentioned, in Amber, templates are compiled and rendered directly in the context of the methods that call `render()`. Those are typically the controller methods themselves, and it means you generally do not need instance variables for passing the information from controllers to views.
521 |
522 | Any variable you define in the controller method, instance or local, is directly visible in the template. For example, let's add the date and time and display them on our "About" page created in the previous step. The controller method and the corresponding view template would look like this:
523 |
524 | ```shell
525 | $ vi src/controllers/page_controller.cr
526 |
527 | def about
528 | time = Time.now
529 | render "about.ecr"
530 | end
531 |
532 | $ vi src/views/page/about.ecr
533 |
534 | Hello, World! The time is now <%= time %>.
535 | ```
536 |
537 | To further confirm that the templates also implicitly run in the same controller objectthat handled the request, you could place e.g. "<%= self.class %> in the above example; the response would be "PageController". So in addition to seeing the method's local variables, it means that all instance variables and methods existing on the controller object are readily available in the templates as well.
538 |
539 | # More on Database Commands
540 |
541 | ## Micrate
542 |
543 | Amber relies on the shard "[micrate](https://github.com/amberframework/micrate)" to perform migrations. The command `amber db` uses "micrate" unconditionally. However, some of all the possible database operations are only available through `amber db` and some are only available through invoking `micrate` directly. Therefore, it is best to prepare the application for using both `amber db` and `micrate`.
544 |
545 | Micrate is primarily a library so a small piece of custom code is required to provide the minimal `micrate` executable for a project. This is done by placing the following in `src/micrate.cr` (the example is for PostgreSQL but could be trivially adapted to MySQL or SQLite):
546 |
547 | ```crystal
548 | #!/usr/bin/env crystal
549 | require "amber"
550 | require "micrate"
551 | require "pg"
552 |
553 | Micrate::DB.connection_url = Amber.settings.database_url
554 | Micrate.logger = Amber.settings.logger.dup
555 | Micrate.logger.progname = "Micrate"
556 |
557 | Micrate::Cli.run
558 | ```
559 |
560 | And by placing the following in `shard.yml` under `targets`:
561 |
562 | ```
563 | targets:
564 | micrate:
565 | main: src/micrate.cr
566 | ```
567 |
568 | From there, running `shards build micrate` would build `bin/micrate` which you could use as an executable to access micrate's functionality directly. Please run `bin/micrate -h` to see an overview of its commands.
569 |
570 | Please note that the described procedure sets up `bin/micrate` and `amber db` in a compatible way so these commands can be used cooperatively and interchangeably.
571 |
572 | To have your database migrations run with different credentials than your regular Amber app, simply create new environments in `config/environments/` and prefix your command lines with `AMBER_ENV=...`. For example, you could copy and modify `config/environments/development.yml` into `config/environments/development_admin.yml`, change the credentials as appropriate, and then run migrations as admin using `AMBER_ENV=development_admin ./bin/amber db migrate`.
573 |
574 | ## Custom Migrations Engine
575 |
576 | While `amber db` unconditionally depends on "micrate", that's the only place where it makes an assumption about the migrations engine used.
577 |
578 | To use a different migrations engine, such as [migrate.cr](https://github.com/vladfaust/migrate.cr), simply perform all database migration work using the engine's native commands instead of using `amber db`. No other adjustments are necessary, and Amber won't get into your way.
579 |
580 | # Internationalization (I18n)
581 |
582 | Amber uses Amber's native shard [citrine-18n](https://github.com/amberframework/citrine-i18n) to provide translation and localization. Even though the shard has been authored by the Amber Framework project, it is Amber-independent and can be used to initialize I18n and determine the visitor's preferred language in any application based on Crystal's HTTP::Server.
583 |
584 | That shard in turn depends on the shard [i18n.cr](https://github.com/TechMagister/i18n.cr) to provide the actual translation and localization functionality.
585 |
586 | The internationalization functionality in Amber is enabled by default. Its setup, initialization, and use basically consist of the following:
587 |
588 | 1. Initializer file `config/initializers/i18n.cr` where basic I18n settings are defined and `I18n.init` is invoked
589 | 1. Locale files in `src/locales/` and subdirectories where settings for both translation and localization are contained
590 | 1. Pipe named `Citrine::I18n::Handler` which is included in `config/routes.cr` and which detects the preferred language for every request based on the value of the request's HTTP header "Accept-Language"
591 | 1. Controller helpers named `t()` and `l()` which provide shorthand access for methods `::I18n.translate` and `::I18n.localize`
592 |
593 | Once the pipe runs on the incoming request, the current request's locale is set in the variable `::I18n.locale`. The value is not stored or copied in any other location and it can be overriden in runtime in any way that the application would require.
594 |
595 | For a locale to be used, it must be requested (or be the default) and exist anywhere under the directory `./src/locales/` with the name `[lang].yml`. If nothing can be found or matched, the locale value will default to "en".
596 |
597 | From there, invoking `t()` and `l()` will perform translation and localization according to the chosen locale. Since these two methods are direct shorthands for methods `::I18n.translate` and `::I18n.localize`, all their usage information and help should be looked up in [i18n.cr's README](https://github.com/TechMagister/i18n.cr).
598 |
599 | In a default Amber application there is a sample localization file `src/locales/en.yml` with one translated string ("Welcome to Amber Framework!") which is displayed as the title on the default project homepage.
600 |
601 | In the future, the default/built-in I18n functionality in Amber might be expanded to automatically organize translations and localizations under subdirectories in `src/locales/` when generators are invoked, just like it is already done for e.g. files in `src/views/`. (This functionality already exists in i18n.cr as explained in [i18n.cr's README](https://github.com/TechMagister/i18n.cr), but is not yet used by Amber.)
602 |
603 | # Responses
604 |
605 | ## Responses with Different Content-Type
606 |
607 | If you want to provide a different format (or a different response altogether) from the controller methods based on accepted content types, you can use `respond_with` from `Amber::Helpers::Responders`.
608 |
609 | Our `about` method from the previous example could be modified in the following way to respond with either HTML or JSON:
610 |
611 | ```crystal
612 | def about
613 | respond_with do
614 | html render "about.ecr"
615 | json name: "John", surname: "Doe"
616 | end
617 | end
618 | ```
619 |
620 | Supported format types are `html`, `json`, `xml`, and `text`. For all the available methods and arguments, please see [controller/helpers/responders.cr](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/responders.cr).
621 |
622 | ## Error Responses
623 |
624 | ### Manual Error Responses
625 |
626 | In any pipe or controller action, you might want to manually return a simple error to the user. This typically means returning an HTTP error code and a short error message, even though you could just as easily print complete pages into the return buffer and return an error code.
627 |
628 | To stop a request during execution and return an error, you would do it this way:
629 |
630 | ```
631 | if some_condition_failed
632 | Amber.logger.error "Error! Returning Bad Request"
633 |
634 | # Status and any headers should be set before writing response body
635 | context.response.status_code = 400
636 |
637 | # Finally, write response body
638 | context.response.puts "Bad Request"
639 |
640 |
641 | # Another way to do the same and respond with a text/plain error
642 | # is to use Crystal's respond_with_error():
643 | context.response.respond_with_error("Bad Request", 400)
644 |
645 | return
646 | end
647 | ```
648 |
649 | Please note that you must use `context.response.puts` or `context.response<<` to print to the output buffer in case of errors. You cannot set the error code and then call `return "Bad Request"` because the return value will not be added to response body if HTTP code is not 2xx.
650 |
651 | ### Error Responses via Error Pipe
652 |
653 | The above example for manually returning error responses does not involve raising any Exceptions — it simply consists of setting the status and response body and returning them to the client.
654 |
655 | Another approach to returning errors consists in using Amber's error subsystem. It automatically provides you with a convenient way to raise Exceptions and return them to the client properly wrapped in application templates etc.
656 |
657 | This method relies on the fact that pipes call next pipes in a row explicitly, and so the method call chain is properly established. In turn, this means that e.g. raising an exception in your controller can be rescued by an earlier pipe in a row that wrapped the call to `call_next(context)` inside a `begin...rescue` block.
658 |
659 | Amber contains a generic pipe named "Errors" for handling errors. It is activated by using the line `plug Amber::Pipe::Error.new` in your `config/routes.cr`.
660 |
661 | To be able to extend the list of errors or modify error templates yourself, you should first run `amber g error` to copy the relevant files to your application. In principle, running this command will get you the files `src/pipes/error.cr`, `src/controllers/error_controller.cr`, and `src/views/error/`, all of which can be modified to suit your needs.
662 |
663 | To see the error subsystem at work, you could now do something as simple as:
664 |
665 | ```crystal
666 | class HomeController < ApplicationController
667 | def index
668 | raise Exception.new "No pass!"
669 | render("index.slang")
670 | end
671 | end
672 | ```
673 |
674 | And then visit [http://localhost:3000/](http://localhost:3000/). You would see a HTTP 500 (Internal Server Error) containing the specified error message, but wrapped in an application template rather than printed plainly like the most basic HTTP errors.
675 |
676 | # Assets Pipeline
677 |
678 | In an Amber project, raw assets are in `src/assets/`:
679 |
680 | ```shell
681 | [[[find app/src/assets/|grep -v gitkeep|sort]]]
682 | ```
683 |
684 | At build time, all these are processed and placed under `public/dist/`.
685 | The JS resources are bundled to `main.bundle.js` and CSS resources are bundled to `main.bundle.css`.
686 |
687 | [Webpack](https://webpack.js.org/) is used for asset management.
688 |
689 | To include additional .js or .css/.scss files you would generally add `import "../../file/path";` statements to `src/assets/javascripts/main.js`. You add both JS and CSS includes into `main.js` because webpack only processes import statements in .js files. So you must add the CSS import lines to a .js file, and as a result, this will produce a JS bundle that contains both JS and CSS data in it. Then, webpack's plugin named ExtractTextPlugin (part of default configuration) is used to extract CSS parts into their own bundle.
690 |
691 | The base/common configuration for all this is in `config/webpack/common.js`.
692 |
693 | ## Adding jQuery and jQuery UI
694 |
695 | As an example, we can add the jQuery and jQuery UI libraries to an Amber project.
696 |
697 | Please note that we are going to unpack the jQuery UI zip file directly into `src/assets/javascripts/` even though it contains some CSS and images. This is done because splitting the different asset types out to individual directories would be harder to do and maintain over time (e.g. paths in jQuery UI CSS files pointing to "images/" would no longer work, and updating the version later would be more complex).
698 |
699 | The whole procedure would be as follows:
700 |
701 | ```bash
702 | cd src/assets/javascripts
703 |
704 | #: Download jQuery
705 | wget https://code.jquery.com/jquery-3.3.1.js
706 |
707 | #: Then download jQuery UI from http://jqueryui.com/download/ to the same/current directory
708 | #: and unpack it:
709 | unzip jquery-ui-1.12.1.custom.zip
710 |
711 | #: Then edit main.js and add the import lines:
712 | import './jquery-3.3.1.min.js'
713 | import './jquery-ui-1.12.1.custom/jquery-ui.css'
714 | import './jquery-ui-1.12.1.custom/jquery-ui.js'
715 | import './jquery-ui-1.12.1.custom/jquery-ui.structure.css'
716 | import './jquery-ui-1.12.1.custom/jquery-ui.theme.css'
717 |
718 | #: And finally, edit ../../../config/webpack/common.js to add jquery resource alias:
719 | resolve: {
720 | alias: {
721 | amber: path.resolve(__dirname, '../../lib/amber/assets/js/amber.js'),
722 | jquery: path.resolve(__dirname, '../../src/assets/javascripts/jquery-3.3.1.min.js')
723 | }
724 | ```
725 |
726 | And that's it. At the next application build (e.g. with `amber watch`) all the mentioned resources and images will be compiled, placed to `public/dist/`, and included in the CSS/JS files.
727 |
728 | ## Resource Aliases
729 |
730 | Sometimes, the code or libraries you include will in turn require other libraries by their generic name, e.g. "jquery". Since a file named "jquery" does not actually exist on disk (or at least not in the location that is searched), this could result in an error such as:
731 |
732 | ```
733 | ERROR in ./src/assets/javascripts/jquery-ui-1.12.1.custom/jquery-ui.js
734 | Module not found: Error: Can't resolve 'jquery' in '.../src/assets/javascripts/jquery-ui-1.12.1.custom'
735 | @ ./src/assets/javascripts/jquery-ui-1.12.1.custom/jquery-ui.js 5:0-26
736 | @ ./src/assets/javascripts/main.js
737 | ```
738 |
739 | The solution is to add resource aliases to webpack's configuration which will instruct it where to find the real files if/when they are referenced by their alias.
740 |
741 | For example, to resolve "jquery", you would add the following to the "resolve" section in `config/webpack/common.js`:
742 |
743 | ```
744 | ...
745 | resolve: {
746 | alias: {
747 | jquery: path.resolve(__dirname, '../../src/assets/javascripts/jquery-3.3.1.min.js')
748 | }
749 | }
750 | ...
751 | ```
752 |
753 | ## CSS Optimization / Minification
754 |
755 | You might want to minimize the CSS that is output to the final CSS bundle.
756 |
757 | To do so you need an entry under "devDependencies" in the project's file `package.json`:
758 |
759 | ```
760 | "optimize-css-assets-webpack-plugin": "^1.3.0",
761 | ```
762 |
763 | And an entry at the top of `config/webpack/common.js`:
764 |
765 | ```
766 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
767 | ```
768 |
769 | And you need to run `npm install` for the plugin to be installed (saved to "node_modules/" subdirectory).
770 |
771 | ## File Copying
772 |
773 | You might also want to copy some of the files from their original location to `public/dist/` without doing any modifications in the process. This is done by adding the following under "devDependencies" in `package.json`:
774 |
775 | ```
776 | "copy-webpack-plugin": "^4.1.1",
777 | ```
778 |
779 | To do so you need following at the top of `config/webpack/common.js`:
780 |
781 | ```
782 | const CopyWebpackPlugin = require('copy-webpack-plugin');
783 | ```
784 |
785 | And the following under "plugins" section down below in the file:
786 |
787 | ```
788 | new CopyWebPackPlugin([
789 | {
790 | from: path.resolve(__dirname, '../../vendor/images/'),
791 | to: path.resolve(__dirname, '../../public/dist/images/'),
792 | ignore: ['.*'],
793 | }
794 | ]),
795 | ```
796 |
797 | And as usual, you need to run `npm install` for the plugin to be installed (saved to "node_modules/" subdirectory).
798 |
799 | ## Asset Management Alternatives
800 |
801 | Maybe it would be useful to replace Webpack with e.g. [Parcel](https://parceljs.org/). (Finding a non-js/non-node/non-npm application for this purpose would be even better; please let me know if you know one.)
802 |
803 | In general it seems it shouldn't be much more complex than replacing the command to run and development dependencies in project's `package.json` file.
804 |
805 | # Advanced Topics
806 |
807 | What follows is a collection of advanced topics which can be read or skipped on an individual basis.
808 |
809 | ## Amber::Controller::Base
810 |
811 | This is the base controller from which all other controllers inherit. Source file is in [controller/base.cr](https://github.com/amberframework/amber/blob/master/src/amber/controller/base.cr).
812 |
813 | On every request, the appropriate controller is instantiated and its initialize() runs. Since this is the base controller, this code runs on every request so you can understand what is available in the context of every controller.
814 |
815 | The content of this controller and the methods it gets from including other modules are intuitive enough to be copied here and commented where necessary:
816 |
817 | ```crystal
818 | [[[cat amber/src/amber/controller/base.cr]]]
819 | ```
820 |
821 | [Helpers::CSRF](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/csrf.cr) module provides:
822 |
823 | ```crystal
824 | def csrf_token
825 | def csrf_tag
826 | def csrf_metatag
827 | ```
828 |
829 | [Helpers::Redirect](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/redirect.cr) module provides:
830 |
831 | ```crystal
832 | def redirect_to(location : String, **args)
833 | def redirect_to(action : Symbol, **args)
834 | def redirect_to(controller : Symbol | Class, action : Symbol, **args)
835 | def redirect_back(**args)
836 | ```
837 |
838 | [Helpers::Render](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/render.cr) module provides:
839 |
840 | ```crystal
841 | LAYOUT = "application.slang"
842 | macro render(template = nil, layout = true, partial = nil, path = "src/views", folder = __FILE__)
843 | ```
844 |
845 | [Helpers::Responders](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/responders.cr) helps control what final status code, body, and content-type will be returned to the client.
846 |
847 | [Helpers::Route](https://github.com/amberframework/amber/blob/master/src/amber/controller/helpers/route.cr) module provides:
848 |
849 | ```crystal
850 | def action_name
851 | def route_resource
852 | def route_scope
853 | def controller_name
854 | ```
855 |
856 | [Callbacks](https://github.com/amberframework/amber/blob/master/src/amber/dsl/callbacks.cr) module provides:
857 |
858 | ```crystal
859 | macro before_action
860 | macro after_action
861 | ```
862 |
863 | ## Extensions
864 |
865 | Amber adds some very convenient extensions to the existing String and Number classes. The extensions are in the [extensions/](https://github.com/amberframework/amber/tree/master/src/amber/extensions) directory. They are useful in general, but particularly so when writing param validation rules. Here's the listing of currently available extensions:
866 |
867 | For String:
868 |
869 | ```crystal
870 | [[[grep 'def ' amber/src/amber/extensions/string.cr]]]
871 | ```
872 |
873 | For Number:
874 |
875 | ```crystal
876 | [[[grep 'def ' amber/src/amber/extensions/number.cr]]]
877 | ```
878 |
879 | ## Shards
880 |
881 | Amber and all of its components depend on the following shards:
882 |
883 | [[[cat shards.txt]]]
884 |
885 | Only the parts that are used end up in the compiled project.
886 |
887 | ## Environments
888 |
889 | After "[amber](https://github.com/amberframework/amber/blob/master/src/amber.cr)" shard is loaded, `Amber` module automatically includes [Amber::Environment](https://github.com/amberframework/amber/blob/master/src/amber/environment.cr) which adds the following methods:
890 |
891 | ```
892 | Amber.settings # Singleton object, contains current settings
893 | Amber.logger # Alias for Amber.settings.logger
894 | Amber.env, Amber.env= # Env (environment) object (development, production, test)
895 | ```
896 |
897 | The list of all available application settings is in [Amber::Environment::Settings](https://github.com/amberframework/amber/blob/master/src/amber/environment/settings.cr). These settings are loaded from the application's `config/environment/.yml` file and are then overriden by any settings in `config/application.cr`'s `Amber::Server.configure` block.
898 |
899 | [Env](https://github.com/amberframework/amber/blob/master/src/amber/environment/env.cr) (`amber.env`) also provides basic methods for querying the current environment:
900 | ```crystal
901 | [[[grep 'def ' amber/src/amber/environment/env.cr]]]
902 | ```
903 |
904 | ## Starting the Server
905 |
906 | It is important to explain exactly what happens from the time you run the application til Amber starts serving user requests:
907 |
908 | 1. `crystal src/.cr` - you or a script starts Amber
909 | 1. `require "../config/*"` - as the first thing, `config/*` is required. Inclusion is in alphabetical order. Crystal only looks for *.cr files and only files in config/ are loaded (no subdirectories)
910 | 1. `require "../config/application.cr"` - this is usually the first file in `config/`
911 | 1. `require "./initializers/**"` - loads all initializers. There is only one initializer file by default, named `initializer/database.cr`. Here we have a double star ("**") meaning inclusion of all files including in subdirectories. Inclusion order is always "current directory first, then subdirectories"
912 | 1. `require "amber"` - Amber itself is loaded
913 | 1. Loading Amber makes `Amber::Server` class available
914 | 1. `include Amber::Environment` - already in this stage, environment is determined and settings are loaded from yml file (e.g. from `config/environments/development.yml`. Settings are later available as `settings`
915 | 1. `require "../src/controllers/application_controller"` - main controller is required. This is the base class for all other controllers
916 | 1. It defines `ApplicationController`, includes JasperHelpers in it, and sets default layout ("application.slang").
917 | 1. `require "../src/controllers/**"` - all other controllers are loaded
918 | 1. `Amber::Server.configure` block is invoked to override any config settings
919 | 1. `require "config/routes.cr"` - this again invokes `Amber::Server.configure` block, but concerns itself with routes and feeds all the routes in
920 | 1. `Amber::Server.start` is invoked
921 | 1. `instance.run` - implicitly creates a singleton instance of server, saves it to `@@instance`, and calls `run` on it
922 | 1. Consults variable `settings.process_count`
923 | 1. If process count is 1, `instance.start` is called
924 | 1. If process count is > 1, the desired number of processes is forked, while main process enters sleep
925 | 1. Forks invoke Process.run() and start completely separate, individual processes which go through the same initialization procedure from the beginning. Forked processes have env variable "FORKED" set to "1", and a variable "id" set to their process number. IDs are assigned in reverse order (highest number == first forked).
926 | 1. `instance.start` is called for every process
927 | 1. It saves current time and prints startup info
928 | 1. `@handler.prepare_pipelines` is called. @handler is Amber::Pipe::Pipeline, a subclass of Crystal's [HTTP::Handler](https://crystal-lang.org/api/0.24.1/HTTP/Handler.html). `prepare_pipelines` is called to connect the pipes so the processing can work, and implicitly adds Amber::Pipe::Controller (the pipe in which app's controller is invoked) as the last pipe. This pipe's duty is to call Amber::Router::Context.process_request, which actually dispatches the request to the controller.
929 | 1. `server = HTTP::Server.new(host, port, @handler)`- Crystal's HTTP server is created
930 | 1. `server.tls = Amber::SSL.new(...).generate_tls if ssl_enabled?`
931 | 1. Signal::INT is trapped (calls `server.close` when received)
932 | 1. `loop do server.listen(settings.port_reuse) end` - server enters main loop
933 |
934 | ## Serving Requests
935 |
936 | Similarly as with starting the server, is important to explain exactly what is happening when Amber is serving requests:
937 |
938 | Amber's app serving model is based on Crystal's built-in, underlying functionality:
939 |
940 | 1. The server that is running is an instance of Crystal's
941 | [HTTP::Server](https://crystal-lang.org/api/0.24.1/HTTP/Server.html)
942 | 2. On every incoming request, a "handler" is invoked. As supported by Crystal, handler can be a simple Proc or an instance of [HTTP::Handler](https://crystal-lang.org/api/0.24.1/HTTP/Handler.html). HTTP::Handlers have a concept of "next" and multiple ones can be connected in a row. In Amber, these individual handlers are called "pipes" and currently at least two of them are always pre-defined — pipes named "Pipeline" and "Controller". The pipe "Pipeline" always executes first; it determines which pipeline the request is meant for and runs the first pipe in that pipeline. The pipe "Controller" always executes last; it consults the routing table, instantiates the appropriate controller, and invokes the appropriate method on it
943 | 3. In the pipeline, every Pipe (Amber::Pipe::*, ultimately subclass of Crystal's [HTTP::Handler](https://crystal-lang.org/api/0.24.2/HTTP/Handler.html)) is invoked with one argument. That argument is
944 | by convention called "context" and it is an instance of `HTTP::Server::Context`. By default it has two built-in methods — `request` and `response`, containing the request and response parts respectively. On top of that, Amber adds various other methods and variables, such as `router`, `flash`, `cookies`, `session`, `content`, `route`, `client_ip`, and others as seen in [router/context.cr](https://github.com/amberframework/amber/blob/master/src/amber/router/context.cr) and [extensions/http.cr](https://github.com/amberframework/amber/blob/master/src/amber/extensions/http.cr)
945 | 4. Please note that calling the chain of pipes is not automatic; every pipe needs to call `call_next(context)` at the appropriate point in its execution to call the next pipe in a row. It is not necessary to check whether the next pipe exists, because currently `Amber::Pipe::Controller` is always implicitly added as the last pipe, so in the context of your pipes the next one always exists. State between pipes is not passed via separate variables but via modifying `context` and the data contained in it. Context persists for the duration of the request. Context persists for the duration of the request
946 |
947 | After that, pipelines, pipes, routes, and other Amber-specific parts come into play.
948 |
949 | So, in detail, from the beginning:
950 |
951 | 1. `loop do server.listen(settings.port_reuse) end` - main loop is running
952 | 1. `spawn handle_client(server.accept?)` - handle_client() is called in a new fiber after connection is accepted
953 | 1. `io = OpenSSL::SSL::Socket::Server.new(io, tls, sync_close: true) if @tls`
954 | 1. `@processor.process(io, io)`
955 | 1. `if request.is_a?(HTTP::Request::BadRequest); response.respond_with_error("Bad Request", 400)`
956 | 1. `response.version = request.version`
957 | 1. `response.headers["Connection"] = "keep-alive" if request.keep_alive?`
958 | 1. `context = Context.new(request, response)` - this context is already extended by Amber with additional properties and methods
959 | 1. `@handler.call(context)` - `Amber::Pipe::Pipeline.call()` is called
960 | 1. `raise ...error... if context.invalid_route?` - route validity is checked early
961 | 1. `if context.websocket?; context.process_websocket_request` - if websocket, parse as such
962 | 1. `elsif ...; ...pipeline.first...call(context)` - if regular HTTP request, call the first handler in the appropriate pipeline
963 | 1. `call_next(context)` - each pipe calls call_next(context) somewhere during its execution, and all pipes are executed
964 | 1. `context.process_request` - the always-last pipe (Amber::Pipe::Controller) calls `process_request` to dispatch the action to controller. After that last pipe, the stack of call_next()s is "unwound" back to the starting position
965 | 1. `context.finalize_response` - minor final adjustments to response are made (headers are added, and response body is printed unless action was HEAD)
966 |
967 | ## Support Routines
968 |
969 | In [support/](https://github.com/amberframework/amber/tree/master/src/amber/support) directory there is a number of various support files that provide additional, ready-made routines.
970 |
971 | Currently, the following can be found there:
972 |
973 | ```
974 | client_reload.cr - Support for reloading developer's browser
975 |
976 | file_encryptor.cr - Support for storing/reading encrypted versions of files
977 | message_encryptor.cr
978 | message_verifier.cr
979 |
980 | locale_formats.cr - Very basic locate data for various manually-added locales
981 |
982 | mime_types.cr - List of MIME types and helper methods for working with them:
983 |
984 | def self.mime_type(format, fallback = DEFAULT_MIME_TYPE)
985 | def self.zip_types(path)
986 | def self.format(accepts)
987 | def self.default
988 | def self.get_request_format(request)
989 | ```
990 |
991 | ## Amber behind a Load Balancer | Reverse Proxy | ADC
992 |
993 | (In this section, the terms "Load Balancer", "Reverse Proxy", "Proxy", and "Application Delivery Controller" (ADC) are used interchangeably.)
994 |
995 | By default, in development environment Amber listens on port 3000, and in production environment it listens on port 8080. This makes it very easy to run a load balancer on ports 80 (HTTP) and 443 (HTTPS) and proxy user requests to Amber.
996 |
997 | There are three groups of benefits of running Amber behind a proxy:
998 |
999 | On a basic level, a proxy will perform TCP and HTTP normalization — it will filter out invalid TCP packets, flags, window sizes, sequence numbers, and SYN floods. It will only pass valid HTTP requests through (protecting the application from protocol-based attacks) and smoothen out deviations which are tolerated by HTTP specification (such as multi-line HTTP headers). Finally, it will provide HTTP/2 support for your application and perform SSL and compression offloading so that these functions are done on the load balancers rather than on the application servers.
1000 |
1001 | Also, as an important implementation-specific detail, Crystal currently does not provide applications with the information on the client IPs that are making HTTP requests. Therefore, Amber is by default unaware of them. With a proxy in front of Amber and using Amber's pipe `ClientIp`, the client IP information will be passed from the proxy to Amber and be available as `context.client_ip.address`.
1002 |
1003 | On an intermediate level, a proxy will provide you with caching and scaling and serve as a versatile TCP and HTTP load balancer. It will cache static files, route your application and database traffic to multiple backend servers, balance multiple protocols based on any criteria, fix and rewrite HTTP traffic, and so on. The benefits of starting application development with acceleration and scaling in mind from the get-go are numerous.
1004 |
1005 | On an advanced level, a proxy will allow you to keep track of arbitrary statistics and counters, perform GeoIP offloading and rate limiting, filter out bots and suspicious web clients, implement DDoS protection and web application firewall, troubleshoot network conditions, and so on.
1006 |
1007 | [HAProxy](www.haproxy.org) is an excellent proxy to use and to run it you will only need the `haproxy` binary, two command line options, and a config file. A simple HAProxy config file that can be used out of the box is available in [support/haproxy.conf](https://github.com/docelic/amber-introduction/blob/master/support/haproxy.conf). This config file will be expanded over time into a full-featured configuration to demonstrate all of the above-mentioned points, but even by default the configuration should be good enough to get you started with practical results.
1008 |
1009 | HAProxy comes pre-packaged for most GNU/Linux distributions and MacOS, but if you do not see version 1.8.x available, it is recommended to manually install the latest stable version.
1010 |
1011 | To compile the latest stable HAProxy from source, you could use the following procedure:
1012 |
1013 | ```
1014 | git clone http://git.haproxy.org/git/haproxy-1.8.git/
1015 | cd haproxy-1.8
1016 | make -j4 TARGET=linux2628 USE_OPENSSL=1
1017 | ```
1018 |
1019 | The compilation will go trouble-free and you will end up with the binary named `haproxy` in the current directory.
1020 |
1021 | To obtain the config file and set up the basic directory structure, please run the following in your Amber app directory:
1022 |
1023 | ```sh
1024 | cd config
1025 | wget https://raw.githubusercontent.com/docelic/amber-introduction/master/support/haproxy.conf
1026 | cd ..
1027 | mkdir -p var/{run,empty}
1028 | ```
1029 |
1030 | And finally, to start HAProxy in development/foreground mode, please run:
1031 |
1032 | ```sh
1033 | sudo ../haproxy-1.8/haproxy -f config/haproxy.conf -d
1034 | ```
1035 |
1036 | And then start `amber watch` and point your browser to [http://localhost/](http://localhost/) instead of [http://localhost:3000/](http://localhost:3000/)!
1037 |
1038 | Please also note that this HAProxy configuration enables the built-in HAProxy status page at [http://localhost/server-status](http://localhost/server-status) and restricts access to it to localhost.
1039 |
1040 | When you confirm everything is working, you can omit the `-d` flag and it will start HAProxy in background, returning the shell back to you. You can then forget about HAProxy until you modify its configuration and want to reload it. Then simply call `kill -USR2 var/run/haproxy.pid`.
1041 |
1042 | Finally, now that we are behind a proxy, to get access to client IPs we can enable the following line in `config/routes.cr`:
1043 |
1044 | ```
1045 | plug Amber::Pipe::ClientIp.new(["X-Forwarded-For"])
1046 | ```
1047 |
1048 | And we can modify one of the views to display the user IP address. Assuming you are using slang, you could edit the default view file `src/views/home/index.slang` and add the following to the bottom to confirm the new behavior:
1049 |
1050 | ```
1051 | a.list-group-item.list-group-item-action href="#" = "IP Address: " + ((ip = context.client_ip) ? ip.address : "Unknown")
1052 | ```
1053 |
--------------------------------------------------------------------------------
/support/amber.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/docelic/amber-introduction/fe417a180b505c265ab5e6a617d836fe78df3b19/support/amber.png
--------------------------------------------------------------------------------
/support/diffs/shards.txt:
--------------------------------------------------------------------------------
1 | # If anything changes here, sync the items in shards.txt
2 | require "amber"
3 | require "amber_router"
4 | require "base64"
5 | require "big"
6 | require "bit_array"
7 | require "callback"
8 | require "citrine-i18n"
9 | require "cli"
10 | require "colorize"
11 | require "compiled_license"
12 | require "compiler/crystal/syntax/*"
13 | require "crikey"
14 | require "crustache"
15 | require "crypto/bcrypt/password"
16 | require "crypto/subtle"
17 | require "db"
18 | require "digest/md5"
19 | require "ecr"
20 | require "ecr/macros"
21 | require "email"
22 | require "exception_page"
23 | require "file_utils"
24 | require "garnet_spec"
25 | require "granite/adapter/<%= @database %>"
26 | require "html"
27 | require "http"
28 | require "http/client"
29 | require "http/headers"
30 | require "http/params"
31 | require "http/server"
32 | require "http/server/handler"
33 | require "i18n"
34 | require "inflector"
35 | require "jasper_helpers"
36 | require "json"
37 | require "kilt"
38 | require "kilt/slang"
39 | require "liquid"
40 | require "logger"
41 | require "markd"
42 | require "micrate"
43 | require "mysql"
44 | require "openssl"
45 | require "openssl/cipher"
46 | require "openssl/hmac"
47 | require "openssl/sha1"
48 | require "optarg"
49 | require "option_parser"
50 | require "pg"
51 | require "pool/connection"
52 | require "process"
53 | require "quartz_mailer"
54 | require "random/secure"
55 | require "redis"
56 | require "selenium"
57 | require "shell-table"
58 | require "slang"
59 | require "socket"
60 | require "socket/tcp_socket"
61 | require "socket/unix_socket"
62 | require "spec"
63 | require "sqlite3"
64 | require "string_inflection/kebab"
65 | require "string_inflection/snake"
66 | require "teeplate"
67 | require "temel"
68 | require "uri"
69 | require "uuid"
70 | require "weak_ref"
71 | require "yaml"
72 | require "zip"
73 | require "zlib"
74 |
--------------------------------------------------------------------------------
/support/haproxy.conf:
--------------------------------------------------------------------------------
1 | global
2 | daemon
3 | master-worker
4 | maxconn 2048
5 | nbproc 1
6 |
7 | pidfile var/run/haproxy.pid
8 | chroot var/empty
9 | resetenv
10 | uid 65534
11 | gid 65534
12 | unix-bind mode 0600 uid 65534 gid 65534
13 |
14 | # Optional descriptive indicators ('app' is taken for LB and app name)
15 | log-tag app1
16 | node app1
17 | description app1
18 |
19 | hard-stop-after 5s
20 | ssl-server-verify required
21 |
22 | defaults
23 | mode http
24 | timeout connect 5s
25 | timeout client 50s
26 | timeout server 50s
27 |
28 | frontend app
29 | bind *:80
30 | default_backend app
31 |
32 | reqidel ^X-Forwarded-For:.*
33 | option forwardfor
34 | option httpclose
35 |
36 | backend app
37 | stats enable
38 | stats uri /server-status
39 | stats refresh 5s
40 | stats admin if LOCALHOST
41 | server development1 127.0.0.1:3000
42 | #server production1 127.0.0.1:8080
43 |
--------------------------------------------------------------------------------
/support/ln-sfs:
--------------------------------------------------------------------------------
1 | ln -sf config etc
2 | ln -sf src/assets
3 | ln -sf src/controllers
4 | ln -sf src/models
5 | ln -sf src/views
6 | ln -sf src/views/layouts
7 |
--------------------------------------------------------------------------------
/support/run-diffs.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ( echo "# If anything changes here, sync the items in shards.txt";
4 | rgrep -h ^require amber/src/ amber/lib/*/src/ app/src/ app/lib/*/src/ |grep -v \\./|grep -v \\.js|sort|uniq ) > diffs/shards.txt
5 |
6 | git diff diffs
7 |
--------------------------------------------------------------------------------
/support/shards.txt:
--------------------------------------------------------------------------------
1 | ```
2 | --------SHARD--------------------SOURCE---DESCRIPTION------------------------------------------------------
3 | ------- Web, Routing, Templates, Mailers, Plugins ---------------------------------------------------------
4 | require "amber" AMBER Amber itself
5 | require "amber_router" AMBER Request router implementation
6 | require "citrine-18n" AMBER Translation and localization
7 | require "http" CRYSTAL Lower-level supporting HTTP functionality
8 | require "http/client" CRYSTAL HTTP Client
9 | require "http/headers" CRYSTAL HTTP Headers
10 | require "http/params" CRYSTAL Collection of HTTP parameters and their values
11 | require "http/server" CRYSTAL HTTP Server
12 | require "http/server/handler" CRYSTAL HTTP Server's support for "handlers" (middleware)
13 | require "quartz_mailer" AMBER Sending and receiving emails
14 | require "email" EXTERNAL Simple email sending library
15 | require "teeplate" AMBER Rendering multiple template files
16 |
17 | ------- Databases and ORM Models --------------------------------------------------------------------------
18 | require "big" EXTERNAL BigRational for numeric. Retains precision, requires LibGMP
19 | require "db" CRYSTAL Common DB API
20 | require "pool/connection" CRYSTAL Part of Crystal's common DB API
21 | require "granite_orm/adapter/<%- @database %>" AMBER Granite's DB-specific adapter
22 | require "micrate" EXTERNAL Database migration tool
23 | require "mysql" CRYSTAL MySQL connector
24 | require "pg" EXTERNAL PostgreSQL connector
25 | require "redis" EXTERNAL Redis client
26 | require "sqlite3" EXTERNAL SQLite3 bindings
27 |
28 | ------- Template Rendering --------------------------------------------------------------------------------
29 | require "crikey" EXTERNAL Template language, Data structure view, inspired by Hiccup
30 | require "crustache" EXTERNAL Template language, {{Mustache}} for Crystal
31 | require "ecr" CRYSTAL Template language, Embedded Crystal (ECR)
32 | require "kilt" EXTERNAL Generic template interface
33 | require "kilt/slang" EXTERNAL Kilt support for Slang template language
34 | require "liquid" EXTERNAL Template language, used by Amber for recipe templates
35 | require "slang" EXTERNAL Template language, inspired by Slim
36 | require "temel" EXTERNAL Template language, extensible Markup DSL
37 |
38 | ------- Command Line, Logs, and Output --------------------------------------------------------------------
39 | require "cli" EXTERNAL Support for building command-line interface applications
40 | require "colorize" CRYSTAL Changing colors and text decorations
41 | require "logger" CRYSTAL Simple but sophisticated logging utility
42 | require "optarg" EXTERNAL Parsing command-line options and arguments
43 | require "option_parser" CRYSTAL Command line options processing
44 | require "shell-table" EXTERNAL Creating text tables in command line terminal
45 |
46 | ------- Formats, Protocols, Digests, and Compression ------------------------------------------------------
47 | require "digest/md5" CRYSTAL MD5 digest algorithm
48 | require "html" CRYSTAL HTML escaping and unescaping methods
49 | require "jasper_helpers" AMBER Helper functions for working with HTML
50 | require "json" CRYSTAL Parsing and generating JSON documents
51 | require "openssl" CRYSTAL OpenSSL integration
52 | require "openssl/hmac" CRYSTAL Computing Hash-based Message Authentication Code (HMAC)
53 | require "openssl/sha1" CRYSTAL OpenSSL SHA1 hash functions
54 | require "yaml" CRYSTAL Serialization and deserialization of YAML 1.1
55 | require "zlib" CRYSTAL Reading/writing Zlib compressed data as specified in RFC 1950
56 |
57 | ------- Supporting Functionality --------------------------------------------------------------------------
58 | require "base64" CRYSTAL Encoding and decoding of binary data using base64 representation
59 | require "bit_array" CRYSTAL Array data structure that compactly stores bits
60 | require "callback" EXTERNAL Defining and invoking callbacks
61 | require "compiled_license" EXTERNAL Compile in LICENSE files from project and dependencies
62 | require "compiler/crystal/syntax/*" CRYSTAL Crystal syntax parser
63 | require "crypto/bcrypt/password" CRYSTAL Generating, reading, and verifying Crypto::Bcrypt hashes
64 | require "crypto/subtle" CRYSTAL
65 | require "file_utils" CRYSTAL Supporting functions for files and directories
66 | require "i18n" EXTERNAL Underlying I18N shard for Crystal
67 | require "inflector" EXTERNAL Inflector for Crystal (a port of Ruby's ActiveSupport::Inflector)
68 | require "process" CRYSTAL Supporting functions for working with system processes
69 | require "random/secure" CRYSTAL Generating random numbers from a secure source provided by system
70 | require "selenium" EXTERNAL Selenium Webdriver client
71 | require "socket" CRYSTAL Supporting functions for working with sockets
72 | require "socket/tcp_socket" CRYSTAL Supporting functions for TCP sockets
73 | require "socket/unix_socket" CRYSTAL Supporting functions for UNIX sockets
74 | require "string_inflection/kebab"EXTERNAL Singular/plurals words in "kebab" style ("foo-bar")
75 | require "string_inflection/snake"EXTERNAL Singular/plurals words in "snake" style ("foo_bar")
76 | require "uri" CRYSTAL Creating and parsing URI references as defined by RFC 3986
77 | require "uuid" CRYSTAL Functions related to Universally unique identifiers (UUIDs)
78 | require "weak_ref" CRYSTAL Weak Reference class allowing referenced objects to be GC-ed
79 | require "zip" EXTERNAL Zip compression library, used for fetching zipped recipes
80 |
81 | require "ecr"
82 | require "markd"
83 | require "exception_page"
84 | ```
85 |
--------------------------------------------------------------------------------
/support/tpl2md.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env perl
2 |
3 | use warnings;
4 | use strict;
5 | use Perl6::Slurp qw/slurp/;
6 | use Fatal qw/open/;
7 |
8 | my @toc;
9 | my %terms= (
10 | 'ameba' => 'veelenga/ameba - Static code analysis (development)',
11 | 'radix' => 'luislavena/radix - Radix Tree implementation',
12 | 'kilt' => 'jeromegn/kilt - Generic template interface',
13 | 'slang' => 'jeromegn/slang - Slang template language',
14 | 'redis' => 'stefanwille/crystal-redis - ',
15 | 'cli' => 'amberframework/cli - Building cmdline apps (based on mosop)',
16 | 'teeplate' => 'amberframework/teeplate - Rendering multiple template files',
17 | 'micrate' => 'juanedi/micrate - Database migration tool',
18 | 'shell-table' => 'jwaldrip/shell-table.cr - Creates textual tables in shell',
19 | 'spinner' => 'askn/spinner - Spinner for the shell',
20 | 'mysql' => 'crystal-lang/crystal-mysql - ',
21 | 'sqlite3' => 'crystal-lang/crystal-sqlite3 - ',
22 | 'pg' => 'will/crystal-pg - PostgreSQL driver',
23 | 'db' => 'crystal-lang/crystal-db - Common DB API',
24 | 'optarg' => 'mosop/optarg - Parsing cmdline args',
25 | 'callback' => 'mosop/callback - Defining and invoking callbacks',
26 | 'string_inflection' => 'mosop/string_inflection - Word plurals, counts, etc.',
27 | 'crystal-db' => 'crystal-lang/crystal-db - Common DB API',
28 | 'smtp.cr' => 'amberframework/smtp.cr - SMTP client (to be replaced with arcage/crystal-email)',
29 | 'selenium-webdriver-crystal' => 'ysbaddaden/selenium-webdriver-crystal - Selenium Webdriver client',
30 | );
31 |
32 | our @shards= `cd amber && shards list | tail -n +2 | awk '{ print \$2 }' | sort |uniq`;
33 |
34 | my $tpl= slurp 'README.md.tpl';
35 |
36 | $tpl=~ s{^(#+)\s+(.*)}{
37 | my( $one, $two) = ($1,$2);
38 | (my $anchor_name= lc $two)=~ s/\W/_/g;
39 | push @toc, ("\t" x (length($one)-1)). '1. ['. $two. "](#$anchor_name)\n";
40 | qq{$one $two}
41 | }gem;
42 | $tpl=~ s/^#: /# /gm;
43 |
44 | $tpl=~ s/\[\[\[(.*?)\]\]\]/`$1`/ge;
45 | $tpl=~ s/\{\{\{TOC\}\}\}/"# Table of Contents\n\n". join( '', @toc)/ge;
46 | $tpl=~ s/\{\{\{SHARDS\}\}\}/replace_keywords( @shards)/ge;
47 |
48 | open my $out, "> ../README.md";
49 | print $out $tpl;
50 | exit 0;
51 |
52 | ###################################
53 | # Helpers below
54 |
55 | sub replace_keywords {
56 | for( @_){
57 | s/^\s+//;
58 | s/\s+$//;
59 | if( $terms{$_}) {
60 | $_= $terms{$_}
61 | }
62 | $_.= "\n";
63 | }
64 | join '', @_
65 | }
66 |
--------------------------------------------------------------------------------