├── .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 | --------------------------------------------------------------------------------