From 4ae28f0f58c7758722a8c9f35ac72703cad51fa4 Mon Sep 17 00:00:00 2001 From: Morgan Aubert Date: Sat, 13 Jul 2024 17:58:45 -0300 Subject: [PATCH] Prepare 0.5.0 release --- Makefile | 2 +- .../how-to/create-custom-cache-stores.md | 2 +- docs/docs/development/applications.md | 6 +- .../how-to/create-custom-model-fields.md | 4 +- .../docs/models-and-databases/introduction.md | 2 +- .../reference/query-set.md | 4 +- .../docs/models-and-databases/transactions.md | 2 +- .../how-to/create-custom-context-producers.md | 2 +- docs/docs/templates/introduction.md | 4 +- docs/docs/the-marten-project/release-notes.md | 2 +- .../the-marten-project/release-notes/0.2.md | 2 +- .../the-marten-project/release-notes/0.3.md | 2 +- .../the-marten-project/release-notes/0.4.md | 2 +- .../the-marten-project/release-notes/0.5.md | 16 +- docs/docusaurus.config.js | 2 +- .../reference/generic-handlers.md | 207 --------- .../reference/middlewares.md | 70 --- .../how-to/create-custom-cache-stores.md | 2 +- .../version-0.3/development/applications.md | 6 +- .../how-to/create-custom-model-fields.md | 4 +- .../models-and-databases/transactions.md | 2 +- .../how-to/create-custom-context-producers.md | 2 +- .../version-0.3/templates/introduction.md | 4 +- .../the-marten-project/release-notes/0.2.md | 2 +- .../the-marten-project/release-notes/0.3.md | 2 +- .../how-to/create-custom-cache-stores.md | 2 +- .../version-0.4/development/applications.md | 6 +- .../how-to/create-custom-model-fields.md | 4 +- .../models-and-databases/introduction.md | 2 +- .../reference/query-set.md | 4 +- .../models-and-databases/transactions.md | 2 +- .../how-to/create-custom-context-producers.md | 2 +- .../version-0.4/templates/introduction.md | 4 +- .../the-marten-project/release-notes/0.2.md | 2 +- .../the-marten-project/release-notes/0.3.md | 2 +- .../the-marten-project/release-notes/0.4.md | 2 +- .../{version-0.2 => version-0.5}/assets.mdx | 0 .../assets/introduction.md | 69 ++- .../authentication.mdx | 0 .../authentication/introduction.md | 58 ++- .../reference/generated-files.md | 13 +- docs/versioned_docs/version-0.5/caching.mdx | 31 ++ .../how-to/create-custom-cache-stores.md | 147 ++++++ .../version-0.5/caching/introduction.md | 153 +++++++ .../version-0.5/caching/reference/stores.md | 46 ++ .../deployment.mdx | 6 + .../how-to/deploy-to-an-ubuntu-server.md | 0 .../deployment/how-to/deploy-to-fly-io.md | 237 ++++++++++ .../deployment/how-to/deploy-to-heroku.md | 215 +++++++++ .../deployment/introduction.md | 9 + .../development.mdx | 9 + .../development/applications.md | 80 +++- .../version-0.5/development/generators.md | 107 +++++ .../how-to/configure-database-backends.md | 156 +++++++ .../how-to/create-custom-commands.md | 42 +- .../development/management-commands.md | 23 +- .../development/reference/generators.md | 194 ++++++++ .../reference/management-commands.md | 98 +++- .../development/reference/settings.md | 226 ++++++++- .../development/settings.md | 10 +- .../development/testing.md | 20 +- .../{version-0.2 => version-0.5}/emailing.mdx | 3 + .../version-0.5/emailing/callbacks.md | 111 +++++ .../how-to/create-custom-emailing-backends.md | 2 +- .../emailing/introduction.md | 52 ++- .../emailing/reference/backends.md | 0 .../{version-0.2 => version-0.5}/files.mdx | 0 .../how-to/create-custom-file-storages.md | 16 +- .../files/managing-files.md | 74 +-- .../files/uploading-files.md | 18 +- .../getting-started.mdx | 0 .../getting-started/installation.md | 30 +- .../getting-started/tutorial.md | 44 +- .../handlers-and-http.mdx | 15 +- .../handlers-and-http/callbacks.md | 173 +++++++ .../version-0.5/handlers-and-http/cookies.md | 150 ++++++ .../handlers-and-http/error-handlers.md | 10 +- .../handlers-and-http/generic-handlers.md | 28 +- .../how-to/create-custom-route-parameters.md | 20 +- .../customize-handler-template-contexts.md | 98 ++++ .../handlers-and-http/introduction.md | 156 +++++-- .../handlers-and-http/middlewares.md | 2 +- .../reference/generic-handlers.md | 328 +++++++++++++ .../reference/middlewares.md | 133 ++++++ .../handlers-and-http/routing.md | 48 +- .../handlers-and-http/sessions.md | 14 +- .../{version-0.2 => version-0.5}/i18n.mdx | 0 .../i18n/introduction.md | 12 + .../models-and-databases.mdx | 6 + .../models-and-databases/callbacks.md | 4 +- .../how-to/create-custom-model-fields.md | 14 +- .../models-and-databases/introduction.md | 114 +++-- .../models-and-databases/migrations.md | 8 +- .../multiple-databases.md | 4 +- .../models-and-databases/queries.md | 196 +++++++- .../models-and-databases/raw-sql.md | 71 ++- .../models-and-databases/reference/fields.md | 110 ++++- .../reference/migration-operations.md | 0 .../reference/query-set.md | 298 +++++++++++- .../reference/table-options.md | 57 +++ .../models-and-databases/relationships.md | 429 ++++++++++++++++++ .../models-and-databases/transactions.md | 8 +- .../models-and-databases/validations.md | 2 +- .../prologue.md => version-0.5/prologue.mdx} | 29 +- .../{version-0.2 => version-0.5}/schemas.mdx | 0 .../how-to/create-custom-schema-fields.md | 8 +- .../schemas/introduction.md | 126 +++-- .../schemas/reference/fields.md | 91 ++++ .../schemas/validations.md | 2 +- .../{version-0.2 => version-0.5}/security.mdx | 3 + .../security/clickjacking.md | 2 +- .../security/content-security-policy.md | 94 ++++ .../security/csrf.md | 22 +- .../security/introduction.md | 8 + .../tutorial/marten_welcome_page.png | Bin .../version-0.5/static/img/prologue/logo.png | Bin 0 -> 16906 bytes .../templates.mdx | 6 + .../how-to/create-custom-context-producers.md | 4 +- .../templates/how-to/create-custom-filters.md | 14 +- .../templates/how-to/create-custom-loaders.md | 29 ++ .../templates/how-to/create-custom-tags.md | 12 +- .../templates/introduction.md | 122 ++++- .../templates/reference/context-producers.md | 8 +- .../templates/reference/filters.md | 48 +- .../templates/reference/loaders.md | 43 ++ .../templates/reference/tags.md | 199 +++++++- .../the-marten-project.mdx | 3 + .../the-marten-project/acknowledgments.md | 2 +- .../the-marten-project/contributing.md | 37 +- .../the-marten-project/design-philosophies.md | 34 ++ .../the-marten-project/release-notes.md | 21 + .../the-marten-project/release-notes/0.1.1.md | 0 .../the-marten-project/release-notes/0.1.2.md | 0 .../the-marten-project/release-notes/0.1.3.md | 0 .../the-marten-project/release-notes/0.1.4.md | 0 .../the-marten-project/release-notes/0.1.5.md | 0 .../the-marten-project/release-notes/0.1.md | 0 .../the-marten-project/release-notes/0.2.1.md | 0 .../the-marten-project/release-notes/0.2.2.md | 0 .../the-marten-project/release-notes/0.2.3.md | 0 .../the-marten-project/release-notes/0.2.4.md | 0 .../the-marten-project/release-notes/0.2.md | 2 +- .../the-marten-project/release-notes/0.3.1.md | 13 + .../the-marten-project/release-notes/0.3.2.md | 14 + .../the-marten-project/release-notes/0.3.3.md | 14 + .../the-marten-project/release-notes/0.3.4.md | 11 + .../the-marten-project/release-notes/0.3.md | 153 +++++++ .../the-marten-project/release-notes/0.4.1.md | 11 + .../the-marten-project/release-notes/0.4.2.md | 11 + .../the-marten-project/release-notes/0.4.3.md | 15 + .../the-marten-project/release-notes/0.4.4.md | 11 + .../the-marten-project/release-notes/0.4.5.md | 13 + .../the-marten-project/release-notes/0.4.md | 174 +++++++ .../the-marten-project/release-notes/0.5.md | 171 +++++++ ...idebars.json => version-0.5-sidebars.json} | 66 ++- docs/versions.json | 4 +- shard.yml | 2 +- src/marten.cr | 2 +- 158 files changed, 6078 insertions(+), 770 deletions(-) delete mode 100644 docs/versioned_docs/version-0.2/handlers-and-http/reference/generic-handlers.md delete mode 100644 docs/versioned_docs/version-0.2/handlers-and-http/reference/middlewares.md rename docs/versioned_docs/{version-0.2 => version-0.5}/assets.mdx (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/assets/introduction.md (73%) rename docs/versioned_docs/{version-0.2 => version-0.5}/authentication.mdx (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/authentication/introduction.md (69%) rename docs/versioned_docs/{version-0.2 => version-0.5}/authentication/reference/generated-files.md (73%) create mode 100644 docs/versioned_docs/version-0.5/caching.mdx create mode 100644 docs/versioned_docs/version-0.5/caching/how-to/create-custom-cache-stores.md create mode 100644 docs/versioned_docs/version-0.5/caching/introduction.md create mode 100644 docs/versioned_docs/version-0.5/caching/reference/stores.md rename docs/versioned_docs/{version-0.2 => version-0.5}/deployment.mdx (64%) rename docs/versioned_docs/{version-0.2 => version-0.5}/deployment/how-to/deploy-to-an-ubuntu-server.md (100%) create mode 100644 docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-fly-io.md create mode 100644 docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-heroku.md rename docs/versioned_docs/{version-0.2 => version-0.5}/deployment/introduction.md (96%) rename docs/versioned_docs/{version-0.2 => version-0.5}/development.mdx (73%) rename docs/versioned_docs/{version-0.2 => version-0.5}/development/applications.md (59%) create mode 100644 docs/versioned_docs/version-0.5/development/generators.md create mode 100644 docs/versioned_docs/version-0.5/development/how-to/configure-database-backends.md rename docs/versioned_docs/{version-0.2 => version-0.5}/development/how-to/create-custom-commands.md (85%) rename docs/versioned_docs/{version-0.2 => version-0.5}/development/management-commands.md (78%) create mode 100644 docs/versioned_docs/version-0.5/development/reference/generators.md rename docs/versioned_docs/{version-0.2 => version-0.5}/development/reference/management-commands.md (62%) rename docs/versioned_docs/{version-0.2 => version-0.5}/development/reference/settings.md (68%) rename docs/versioned_docs/{version-0.2 => version-0.5}/development/settings.md (88%) rename docs/versioned_docs/{version-0.2 => version-0.5}/development/testing.md (93%) rename docs/versioned_docs/{version-0.2 => version-0.5}/emailing.mdx (86%) create mode 100644 docs/versioned_docs/version-0.5/emailing/callbacks.md rename docs/versioned_docs/{version-0.2 => version-0.5}/emailing/how-to/create-custom-emailing-backends.md (89%) rename docs/versioned_docs/{version-0.2 => version-0.5}/emailing/introduction.md (67%) rename docs/versioned_docs/{version-0.2 => version-0.5}/emailing/reference/backends.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/files.mdx (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/files/how-to/create-custom-file-storages.md (88%) rename docs/versioned_docs/{version-0.2 => version-0.5}/files/managing-files.md (79%) rename docs/versioned_docs/{version-0.2 => version-0.5}/files/uploading-files.md (81%) rename docs/versioned_docs/{version-0.2 => version-0.5}/getting-started.mdx (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/getting-started/installation.md (59%) rename docs/versioned_docs/{version-0.2 => version-0.5}/getting-started/tutorial.md (95%) rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http.mdx (76%) create mode 100644 docs/versioned_docs/version-0.5/handlers-and-http/callbacks.md create mode 100644 docs/versioned_docs/version-0.5/handlers-and-http/cookies.md rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/error-handlers.md (88%) rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/generic-handlers.md (84%) rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/how-to/create-custom-route-parameters.md (76%) create mode 100644 docs/versioned_docs/version-0.5/handlers-and-http/how-to/customize-handler-template-contexts.md rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/introduction.md (64%) rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/middlewares.md (92%) create mode 100644 docs/versioned_docs/version-0.5/handlers-and-http/reference/generic-handlers.md create mode 100644 docs/versioned_docs/version-0.5/handlers-and-http/reference/middlewares.md rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/routing.md (82%) rename docs/versioned_docs/{version-0.2 => version-0.5}/handlers-and-http/sessions.md (72%) rename docs/versioned_docs/{version-0.2 => version-0.5}/i18n.mdx (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/i18n/introduction.md (90%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases.mdx (87%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/callbacks.md (97%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/how-to/create-custom-model-fields.md (95%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/introduction.md (83%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/migrations.md (98%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/multiple-databases.md (95%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/queries.md (64%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/raw-sql.md (67%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/reference/fields.md (74%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/reference/migration-operations.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/reference/query-set.md (64%) create mode 100644 docs/versioned_docs/version-0.5/models-and-databases/reference/table-options.md create mode 100644 docs/versioned_docs/version-0.5/models-and-databases/relationships.md rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/transactions.md (89%) rename docs/versioned_docs/{version-0.2 => version-0.5}/models-and-databases/validations.md (99%) rename docs/versioned_docs/{version-0.2/prologue.md => version-0.5/prologue.mdx} (71%) rename docs/versioned_docs/{version-0.2 => version-0.5}/schemas.mdx (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/schemas/how-to/create-custom-schema-fields.md (97%) rename docs/versioned_docs/{version-0.2 => version-0.5}/schemas/introduction.md (63%) rename docs/versioned_docs/{version-0.2 => version-0.5}/schemas/reference/fields.md (51%) rename docs/versioned_docs/{version-0.2 => version-0.5}/schemas/validations.md (99%) rename docs/versioned_docs/{version-0.2 => version-0.5}/security.mdx (79%) rename docs/versioned_docs/{version-0.2 => version-0.5}/security/clickjacking.md (97%) create mode 100644 docs/versioned_docs/version-0.5/security/content-security-policy.md rename docs/versioned_docs/{version-0.2 => version-0.5}/security/csrf.md (84%) rename docs/versioned_docs/{version-0.2 => version-0.5}/security/introduction.md (84%) rename docs/versioned_docs/{version-0.2 => version-0.5}/static/img/getting-started/tutorial/marten_welcome_page.png (100%) create mode 100644 docs/versioned_docs/version-0.5/static/img/prologue/logo.png rename docs/versioned_docs/{version-0.2 => version-0.5}/templates.mdx (81%) rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/how-to/create-custom-context-producers.md (85%) rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/how-to/create-custom-filters.md (90%) create mode 100644 docs/versioned_docs/version-0.5/templates/how-to/create-custom-loaders.md rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/how-to/create-custom-tags.md (94%) rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/introduction.md (73%) rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/reference/context-producers.md (87%) rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/reference/filters.md (56%) create mode 100644 docs/versioned_docs/version-0.5/templates/reference/loaders.md rename docs/versioned_docs/{version-0.2 => version-0.5}/templates/reference/tags.md (50%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project.mdx (79%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/acknowledgments.md (95%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/contributing.md (81%) create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/design-philosophies.md rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes.md (54%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.1.1.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.1.2.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.1.3.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.1.4.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.1.5.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.1.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.2.1.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.2.2.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.2.3.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.2.4.md (100%) rename docs/versioned_docs/{version-0.2 => version-0.5}/the-marten-project/release-notes/0.2.md (98%) create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.1.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.2.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.3.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.4.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.1.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.2.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.3.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.4.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.5.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.md create mode 100644 docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.5.md rename docs/versioned_sidebars/{version-0.2-sidebars.json => version-0.5-sidebars.json} (76%) diff --git a/Makefile b/Makefile index 978a56c80..66f25cd4a 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,9 @@ docs: docs_api docs_site .PHONY: docs_api docs_api: crystal docs --output=docs/static/api/dev - cp -R docs/static/api/dev/ docs/static/api/0.2 cp -R docs/static/api/dev/ docs/static/api/0.3 cp -R docs/static/api/dev/ docs/static/api/0.4 + cp -R docs/static/api/dev/ docs/static/api/0.5 .PHONY: docs_site docs_site: diff --git a/docs/docs/caching/how-to/create-custom-cache-stores.md b/docs/docs/caching/how-to/create-custom-cache-stores.md index 30287dbd7..aad0a46b4 100644 --- a/docs/docs/caching/how-to/create-custom-cache-stores.md +++ b/docs/docs/caching/how-to/create-custom-cache-stores.md @@ -138,7 +138,7 @@ end ## Enabling the use of custom cache stores -Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache-store1) setting. +Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache_store) setting. For example: diff --git a/docs/docs/development/applications.md b/docs/docs/development/applications.md index b396adcb8..0db08c997 100644 --- a/docs/docs/development/applications.md +++ b/docs/docs/development/applications.md @@ -16,7 +16,7 @@ Another benefit of applications is that they can be packaged and reused across m ## Using applications -The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installedapps) setting. +The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installed_apps) setting. This setting corresponds to an array of installed app classes. Indeed, each Marten application must define a subclass of [`Marten::App`](pathname:///api/dev/Marten/App.html) to specify a few things such as the application label (see [Creating applications](#creating-applications) for more information about this). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. @@ -40,7 +40,7 @@ Adding an application class inside this array will have the following impact on ### The main application -The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installedapps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. +The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installed_apps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. :::info The main application is associated with the `main` label. This means that models of the main application that do not define an explicit table name will have table names starting with `main_`. @@ -52,7 +52,7 @@ In the end, the main application provides a convenient way for starting projects ### Order of installed applications -You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installedapps) setting can actually matter. +You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installed_apps) setting can actually matter. For example, a "foo" app might define a `test.html` template, and a similar template with the exact same name might be defined by a "bar" app. If the "foo" app appears before the "bar" app in the array of installed apps, then requesting and rendering the `test.html` template will actually involve the "foo" app's template only. This is because template loaders associated with app directories iterate over applications in the order in which they are defined in the installed apps array. diff --git a/docs/docs/models-and-databases/how-to/create-custom-model-fields.md b/docs/docs/models-and-databases/how-to/create-custom-model-fields.md index 7b174e71e..8295bdce7 100644 --- a/docs/docs/models-and-databases/how-to/create-custom-model-fields.md +++ b/docs/docs/models-and-databases/how-to/create-custom-model-fields.md @@ -137,7 +137,7 @@ def from_db_result_set(result_set : ::DB::ResultSet) : ::UUID? end ``` -The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#fromdb) once you get the value from the database result set in order to return the final value. +The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#from_db) once you get the value from the database result set in order to return the final value. #### `to_column` @@ -163,7 +163,7 @@ If for some reason your custom field does not contribute any columns to the data #### `to_db` -The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#fromdb) method. +The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#from_db) method. For example, this method could return the string representation of a `UUID` object: diff --git a/docs/docs/models-and-databases/introduction.md b/docs/docs/models-and-databases/introduction.md index a78371371..a3b4e1ad3 100644 --- a/docs/docs/models-and-databases/introduction.md +++ b/docs/docs/models-and-databases/introduction.md @@ -405,7 +405,7 @@ Please head over to the [Model validations](./validations.md) guide in order to Model classes can inherit from each other. This allows you to easily reuse the field definitions and table attributes of a parent model within a child model. -Presently, the Marten web framework allows [abstract model inheritance](#inheriting-from-abstract-models) (which is useful in order to reuse shared model fields and patterns over multiple child models without having a database table created for the parent model) and [multi-table inheritance](#multi-table-inheritance). +Presently, the Marten web framework allows [abstract model inheritance](#abstract-model-inheritance) (which is useful in order to reuse shared model fields and patterns over multiple child models without having a database table created for the parent model) and [multi-table inheritance](#multi-table-inheritance). ### Abstract model inheritance diff --git a/docs/docs/models-and-databases/reference/query-set.md b/docs/docs/models-and-databases/reference/query-set.md index a545846a5..168ca1dfb 100644 --- a/docs/docs/models-and-databases/reference/query-set.md +++ b/docs/docs/models-and-databases/reference/query-set.md @@ -123,7 +123,7 @@ query_set_1 = Post.all.distinct query_set_2 = Post.all.distinct(:title) ``` -It should be noted that it is also possible to follow associations of direct related models too by using the [double underscores notation](../queries.md#joins-and-filtering-relations) (`__`). For example the following query will select distinct records based on a joined "author" attribute: +It should be noted that it is also possible to follow associations of direct related models too by using the [double underscores notation](../queries.md#filtering-relations) (`__`). For example the following query will select distinct records based on a joined "author" attribute: ``` query_set = Post.all.distinct(:author__name) @@ -171,7 +171,7 @@ query_set.filter { (q(name: "Foo") | q(name: "Bar")) & q(is_published: True) } ### `join` -Returns a queryset whose specified `relations` are "followed" and joined to each result (see [Queries](../queries.md#joins-and-filtering-relations) for an introduction about this capability). +Returns a queryset whose specified `relations` are "followed" and joined to each result (see [Queries](../queries.md#filtering-relations) for an introduction about this capability). When using `#join`, the specified relationships will be followed and each record returned by the queryset will have the corresponding related objects already selected and populated. Using `#join` can result in performance improvements since it can help reduce the number of SQL queries, as illustrated by the following example: diff --git a/docs/docs/models-and-databases/transactions.md b/docs/docs/models-and-databases/transactions.md index 4721ed945..d228f9616 100644 --- a/docs/docs/models-and-databases/transactions.md +++ b/docs/docs/models-and-databases/transactions.md @@ -43,7 +43,7 @@ When transaction blocks are nested, this results in all the database statements Basic model operations such as [creating](./introduction.md#create), [updating](./introduction.md#update), or [deleting](./introduction.md#delete) records are automatically wrapped in a transaction. This helps in ensuring that any exception that is raised in the context of validations or as part of `after_*` [callbacks](./callbacks.md) (ie. `after_create`, `after_update`, `after_save`, and `after_delete`) will also roll back the current transaction. -The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#aftercommit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). +The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#after_commit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). ## Exception handling and rollbacks diff --git a/docs/docs/templates/how-to/create-custom-context-producers.md b/docs/docs/templates/how-to/create-custom-context-producers.md index 3d7c892e3..dd3bec162 100644 --- a/docs/docs/templates/how-to/create-custom-context-producers.md +++ b/docs/docs/templates/how-to/create-custom-context-producers.md @@ -25,4 +25,4 @@ end ## Activating context producers -As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#contextproducers) setting in order to be used by the Marten templates engine when initializing new context objects. +As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#context_producers) setting in order to be used by the Marten templates engine when initializing new context objects. diff --git a/docs/docs/templates/introduction.md b/docs/docs/templates/introduction.md index fa112bce9..583e6e03c 100644 --- a/docs/docs/templates/introduction.md +++ b/docs/docs/templates/introduction.md @@ -226,7 +226,7 @@ Obviously, it is also possible to include partials that don't require any variab Templates can be loaded from specific locations within your codebase and from application folders. This is controlled by two main settings: * [`templates.app_dirs`](../development/reference/settings.md#app_dirs-1) is a boolean that indicates whether or not it should be possible to load templates that are provided by [installed applications](../development/reference/settings.md#installed_apps). Indeed, applications can define a `templates` folder at their root, and these templates will be discoverable by Marten if this setting is set to `true` -* [`templates.dirs`](../development/reference/settings.md#dirs1) is an array of additional directories where templates should be looked for +* [`templates.dirs`](../development/reference/settings.md#dirs-1) is an array of additional directories where templates should be looked for Application templates are always enabled by default (`templates.app_dirs = true`) for new Marten projects. @@ -387,7 +387,7 @@ Context producers are helpers that ensure that common variables are automaticall For example, they can be used to insert the current HTTP request object in every template context being rendered in the context of a handler and HTTP request. This makes sense considering that the HTTP request object is a common object that is likely to be used by multiple templates in your project: that way there is no need to explicitly "insert" it in the context every time you render a template. This specific capability is provided by the [`Marten::Template::ContextProducer::Request`](pathname:///api/dev/Marten/Template/ContextProducer/Request.html) context producer, which inserts a `request` object into every template context. -Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#contextproducers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: +Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#context_producers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: ```crystal config.templates.context_producers = [ diff --git a/docs/docs/the-marten-project/release-notes.md b/docs/docs/the-marten-project/release-notes.md index d7878bfbc..7450a11d6 100644 --- a/docs/docs/the-marten-project/release-notes.md +++ b/docs/docs/the-marten-project/release-notes.md @@ -8,7 +8,7 @@ Here are listed the release notes for each version of the Marten web framework. ## Marten 0.5 -* [Marten 0.5 release notes](./release-notes/0.5.md) _(under development)_ +* [Marten 0.5 release notes](./release-notes/0.5.md) ## Marten 0.4 diff --git a/docs/docs/the-marten-project/release-notes/0.2.md b/docs/docs/the-marten-project/release-notes/0.2.md index 039c9b614..434c74ee1 100644 --- a/docs/docs/the-marten-project/release-notes/0.2.md +++ b/docs/docs/the-marten-project/release-notes/0.2.md @@ -73,7 +73,7 @@ end ### Transaction callbacks -Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#aftercommit) and [`#after_rollback`](../../models-and-databases/callbacks.md#afterrollback) macros. +Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#after_commit) and [`#after_rollback`](../../models-and-databases/callbacks.md#after_rollback) macros. For example: diff --git a/docs/docs/the-marten-project/release-notes/0.3.md b/docs/docs/the-marten-project/release-notes/0.3.md index 7bedfef92..3bb211034 100644 --- a/docs/docs/the-marten-project/release-notes/0.3.md +++ b/docs/docs/the-marten-project/release-notes/0.3.md @@ -127,7 +127,7 @@ end #### Development -* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowedhosts) setting). +* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) setting). * [`Marten#setup`](pathname:///api/0.3/Marten.html#setup-class-method) now raises.[`Marten::Conf::Errors::InvalidConfiguration`](pathname:///api/0.3/Marten/Conf/Errors/InvalidConfiguration.html) exceptions when a configured database involves a backend that is not installed (eg. a MySQL database configured without `crystal-lang/crystal-mysql` installed and required). * The [`new`](../../development/reference/management-commands.md#new) management command now automatically creates a [`.editorconfig`](https://editorconfig.org) file for new projects. * A new [`root_path`](../../development/reference/settings.md#root_path) setting was introduced to make it possible to configure the actual location of the project sources in your system. This is especially useful when deploying projects that have been compiled in a different location from their final destination, which can happen on platforms like Heroku. By setting the root path, you can ensure that your application can find all necessary project sources, as well as other files like locales, assets, and templates. diff --git a/docs/docs/the-marten-project/release-notes/0.4.md b/docs/docs/the-marten-project/release-notes/0.4.md index 79894a573..434e30518 100644 --- a/docs/docs/the-marten-project/release-notes/0.4.md +++ b/docs/docs/the-marten-project/release-notes/0.4.md @@ -134,7 +134,7 @@ end * The hash of matched routing parameters that is available from handlers through the use of the `#params` method can accept symbols and strings when performing key lookups. * The [GZip middleware](../../handlers-and-http/reference/middlewares.md#gzip-middleware) now incorporates a mitigation strategy against the BREACH attack. This strategy (described in the [Heal The Breach paper](https://ieeexplore.ieee.org/document/9754554)) involves introducing up to 100 random bytes into GZip responses to enhance the security against such attacks. * A new [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback type is now available to handlers. Such callbacks are executed before rendering a [template](../../templates/introduction.md) in order to produce a response. As such they are well suited for adding new variables to the global template context so that they are available to the template runtime. -* All handlers now have access to a [global template context](../../handlers-and-http/introduction.md#global-template-context) through the use of the [`#context`](pathname:///api/0.4/Marten/Handlers/Base.html#context-instance-method) method. This template context object is available for the lifetime of the considered handler and can be mutated to define which variables are made available to the template runtime when rendering templates (either through the use of the [`#render`](#render) helper method or when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/generic-handlers.md#rendering-a-template) generic handler). This feature can be combined with the [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback to effortlessly introduce new variables to the context used for rendering a template and generating a handler response. +* All handlers now have access to a [global template context](../../handlers-and-http/introduction.md#global-template-context) through the use of the [`#context`](pathname:///api/0.4/Marten/Handlers/Base.html#context-instance-method) method. This template context object is available for the lifetime of the considered handler and can be mutated to define which variables are made available to the template runtime when rendering templates (either through the use of the [`#render`](../../handlers-and-http/introduction.md#render) helper method or when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/generic-handlers.md#rendering-a-template) generic handler). This feature can be combined with the [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback to effortlessly introduce new variables to the context used for rendering a template and generating a handler response. #### Templates diff --git a/docs/docs/the-marten-project/release-notes/0.5.md b/docs/docs/the-marten-project/release-notes/0.5.md index 43e5d64f0..7e4dedfba 100644 --- a/docs/docs/the-marten-project/release-notes/0.5.md +++ b/docs/docs/the-marten-project/release-notes/0.5.md @@ -4,7 +4,7 @@ pagination_prev: null pagination_next: null --- -_Under development._ +_July 13, 2024._ ## Requirements and compatibility @@ -41,7 +41,7 @@ Please refer to [Pre-fetching relations](../../models-and-databases/queries.md#p It is now possible to define [scopes](../../models-and-databases/queries.md#scopes) in model classes. Scopes allow to pre-define specific filtered query sets, which can be easily applied to model classes and model query sets. -Such scopes can be defined through the use of the [`#scope`](pathname:///api/dev/Marten/DB/Model/Querying.html#scope(name%2C%26block)-macro) macro, which expects a scope name (string literal or symbol) as first argument and requires a block where the query set filtering logic is defined: +Such scopes can be defined through the use of the [`#scope`](pathname:///api/0.5/Marten/DB/Model/Querying.html#scope(name%2C%26block)-macro) macro, which expects a scope name (string literal or symbol) as first argument and requires a block where the query set filtering logic is defined: ```crystal class Post < Marten::Model @@ -61,7 +61,7 @@ end Post.published # => Post::QuerySet [...]> ``` -It is also possible to override the default scope through the use of the [`#default_scope`](pathname:///api/dev/Marten/DB/Model/Querying.html#default_scope-macro) macro. This macro requires a block where the query set filtering logic is defined: +It is also possible to override the default scope through the use of the [`#default_scope`](pathname:///api/0.5/Marten/DB/Model/Querying.html#default_scope-macro) macro. This macro requires a block where the query set filtering logic is defined: ```crystal class Post < Marten::Model @@ -117,7 +117,7 @@ Please refer to [Filtering with raw SQL predicates](../../models-and-databases/r #### Models and databases * A new [`#pks`](../../models-and-databases/reference/query-set.md#pks) method was introduced for query sets to make it easy to retrieve the primary key values of the model records targeted by a given current query set. -* A [`#count`](pathname:///api/dev/Marten/DB/Model/Querying/ClassMethods.html#count(field%3AString|Symbol|Nil%3Dnil)-instance-method) method is now available on model classes and provides the same functionality as the [`#count`](../../models-and-databases/reference/query-set.md#count) query set method. +* A [`#count`](pathname:///api/0.5/Marten/DB/Model/Querying/ClassMethods.html#count(field%3AString|Symbol|Nil%3Dnil)-instance-method) method is now available on model classes and provides the same functionality as the [`#count`](../../models-and-databases/reference/query-set.md#count) query set method. * A new [`#bulk_create`](../../models-and-databases/reference/query-set.md#bulk_create) method was introduced to make it easy to insert multiple model instances into the database in a single query (which can be useful when dealing with large amounts of data that need to be inserted into the database). * A new [`#average`](../../models-and-databases/reference/query-set.md#average) method was introduced to allow computing the average values of a specific model field at the database level for the records targeted by a specific query set. * A new [`#sum`](../../models-and-databases/reference/query-set.md#sum) method was introduced to allow computing the sum of the values of a specific model field at the database level for the records targeted by a specific query set. @@ -125,17 +125,17 @@ Please refer to [Filtering with raw SQL predicates](../../models-and-databases/r * The [`in`](../../models-and-databases/reference/query-set.md#in) query set predicate now supports filtering on arrays of model records directly. * Query sets now provide a [`#to_sql`](../../models-and-databases/reference/query-set.md#to_sql) method allowing to retrieve the corresponding SQL representation. * Query sets can now be combined using the AND and OR binary operators. This can be achieved through the use of the [&](../../models-and-databases/reference/query-set.md#-and) and [`|`](../../models-and-databases/reference/query-set.md#-or) query set methods. -* Invalid record exceptions (instances of [`Marten::DB::Errors::InvalidRecord`](pathname:///api/dev/Marten/DB/Errors/InvalidRecord.html)) now provide more details regarding the actual errors of the associated record. +* Invalid record exceptions (instances of [`Marten::DB::Errors::InvalidRecord`](pathname:///api/0.5/Marten/DB/Errors/InvalidRecord.html)) now provide more details regarding the actual errors of the associated record. * Creations of records from many-to-one reverse relation query sets are now scoped to the related record. See [Many-to-one relationships](../../models-and-databases/relationships.md#many-to-one-relationships) for more details. * A new [`#build`](../../models-and-databases/reference/query-set.md#build) method was introduced to make it possible to initialize new model instances from query sets. #### Handlers and HTTP * The [`#render`](../../handlers-and-http/introduction.md#render) helper method and the generic handlers that involve template renderings now automatically insert the request object in the template context. -* The [`Marten::Handlers::RecordDetail`](../../handlers-and-http/reference/generic-handlers.md#displaying-a-record), [`Marten::Handlers::RecordUpdate`](../../handlers-and-http/reference/generic-handlers.md#updating-a-record), and [`Marten::Handlers::RecordDelete`](../../handlers-and-http/reference/generic-handlers.md#deleting-a-record) generic handlers now provide the ability to specify a custom [query set](../../models-and-databases/queries.md) instead of a model class. This can be achieved through the use of the [`#queryset`](pathname:///api/dev/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro. +* The [`Marten::Handlers::RecordDetail`](../../handlers-and-http/reference/generic-handlers.md#displaying-a-record), [`Marten::Handlers::RecordUpdate`](../../handlers-and-http/reference/generic-handlers.md#updating-a-record), and [`Marten::Handlers::RecordDelete`](../../handlers-and-http/reference/generic-handlers.md#deleting-a-record) generic handlers now provide the ability to specify a custom [query set](../../models-and-databases/queries.md) instead of a model class. This can be achieved through the use of the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro. * A new middleware was introduced to make it easy to override the method of incoming requests based on the value of a specific request parameter or header: the [method override middleware](../../handlers-and-http/reference/middlewares.md#method-override-middleware). This mechanism is useful for overriding HTTP methods in HTML forms that natively support GET and POST methods only. A dedicated set of [settings](../../development/reference/settings.md#method-overriding-settings) is also available to easily customize the behavior of this middleware. * The value of the max-age directive used for the Cache-Control header that is set by the [assets serving middleware](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) can now be configured in the [`assets.max_age`](../../development/reference/settings.md#max_age) setting. -* Subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/reference/generic-handlers.md#rendering-a-template) generic handler now support a [`#content_type`](pathname:///api/dev/Marten/Handlers/Rendering/ClassMethods.html#content_type(content_type%3AString|Nil)-instance-method) class method that allows configuring the content type of the response (unless specified, the content type will default to `text/html`). +* Subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/reference/generic-handlers.md#rendering-a-template) generic handler now support a [`#content_type`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#content_type(content_type%3AString|Nil)-instance-method) class method that allows configuring the content type of the response (unless specified, the content type will default to `text/html`). #### Templates @@ -168,4 +168,4 @@ Please refer to [Filtering with raw SQL predicates](../../models-and-databases/r ### Handlers and HTTP -* Handlers making use of the [`Marten::Handlers::Schema`](pathname:///api/dev/Marten/Handlers/Schema.html) generic handler (or schemas inheriting from it, such as [`Marten::Handlers::RecordCreate`](pathname:///api/dev/Marten/Handlers/RecordCreate.html) or [`Marten::Handlers::RecordUpdate`](pathname:///api/dev/Marten/Handlers/RecordUpdate.html)) will now return a **422 Unprocessable Content** response instead of a **200 OK** response when processing an invalid schema. While this won't have any incidence on end users, this change may make some specs fail if you were testing the response code of handlers that process invalid schema data. The way to fix this will be to replace `200` by `422` in the impacted specs. +* Handlers making use of the [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) generic handler (or schemas inheriting from it, such as [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) or [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html)) will now return a **422 Unprocessable Content** response instead of a **200 OK** response when processing an invalid schema. While this won't have any incidence on end users, this change may make some specs fail if you were testing the response code of handlers that process invalid schema data. The way to fix this will be to replace `200` by `422` in the impacted specs. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 028a6cd97..f0f79dcbd 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -53,7 +53,7 @@ const darkTheme = themes.dracula; dropdownActiveClassDisabled: true, }, { - href: 'https://martenframework.com/docs/api/0.4/index.html', + href: 'https://martenframework.com/docs/api/0.5/index.html', label: 'API', position: 'right', }, diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/reference/generic-handlers.md b/docs/versioned_docs/version-0.2/handlers-and-http/reference/generic-handlers.md deleted file mode 100644 index 6ddcd1b93..000000000 --- a/docs/versioned_docs/version-0.2/handlers-and-http/reference/generic-handlers.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Generic handlers -description: Generic handlers reference ---- - -This page provides a reference for all the available [generic handlers](../generic-handlers.md). - -## Creating a record - -**Class:** [`Marten::Handlers::RecordCreate`](pathname:///api/0.2/Marten/Handlers/RecordCreate.html) - -Handler allowing to create a new model record by processing a schema. - -This handler can be used to process a form, validate its data through the use of a [schema](../../schemas.mdx), and create a record by using the validated data. It is expected that the handler will be accessed through a GET request first: when this happens the configured template is rendered and displayed, and the configured schema which is initialized can be accessed from the template context in order to render a form for example. When the form is submitted via a POST request, the configured schema is validated using the form data. If the data is valid, the corresponding model record is created and the handler returns an HTTP redirect to a configured success URL. - -```crystal -class MyFormHandler < Marten::Handlers::RecordCreate - model MyModel - schema MyFormSchema - template_name "my_form.html" - success_route_name "my_form_success" -end -``` - -It should be noted that the redirect response issued will be a 302 (found). - -The model class used to create the new record can be configured through the use of the [`#model`](pathname:///api/0.2/Marten/Handlers/RecordCreate.html#model(model%3ADB%3A%3AModel.class%3F)-class-method) class method. The schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.2/Marten/Handlers/Schema.html#schema(schema%3AMarten%3A%3ASchema.class%3F)-class-method) class method. Alternatively, the [`#schema_class`](pathname:///api/0.2/Marten/Handlers/Schema.html#schema_class-instance-method) method can also be overridden to dynamically define the schema class as part of the request handler handling. - -The [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the schema while the [`#success_route_name`](pathname:///api/0.2/Marten/Handlers/Schema.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the schema has been validated. Alternatively, the [`#sucess_url`](pathname:///api/0.2/Marten/Handlers/Schema.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.2/Marten/Handlers/Schema.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. - -## Deleting a record - -**Class:** [`Marten::Handlers::RecordDelete`](pathname:///api/0.2/Marten/Handlers/RecordDelete.html) - -Handler allowing to delete a specific model record. - -This handler can be used to delete an existing model record by issuing a POST request. Optionally the handler can be accessed with a GET request and a template can be displayed in this case; this allows to display a confirmation page to users before deleting the record: - -```crystal -class ArticleDeleteHandler < Marten::Handlers::RecordDelete - model MyModel - template_name "article_delete.html" - success_route_name "article_delete_success" -end -``` - -It should be noted that the redirect response issued will be a 302 (found). - -The [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render a deletion confirmation page while the [`#success_route_name`](pathname:///api/0.2/Marten/Handlers/RecordDelete.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the deletion is complete. Alternatively, the [`#sucess_url`](pathname:///api/0.2/Marten/Handlers/RecordDelete.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.2/Marten/Handlers/RecordDelete.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. - -## Displaying a record - -**Class:** [`Marten::Handlers::RecordDetail`](pathname:///api/0.2/Marten/Handlers/RecordDetail.html) - -Handler allowing to display a specific model record. - -This handler can be used to retrieve a [model](../../models-and-databases/introduction.md) record, and to display it as part of a [rendered template](../../templates.mdx). - -```crystal -class ArticleDetailHandler < Marten::Handlers::RecordDetail - model Article - template_name "articles/detail.html" -end -``` - -The model class used to retrieve the record can be configured through the use of the [`#model`](pathname:///api/0.2/Marten/Handlers/RecordDetail.html#model%3ADB%3A%3AModel.class%3F-class-method) class method. By default, a [`Marten::Handlers::RecordDetail`](pathname:///api/0.2/Marten/Handlers/RecordDetail.html) subclass will always retrieve model records by looking for a `pk` route parameter: this parameter is assumed to contain the value of the primary key field associated with the record that should be rendered. If you need to use a different route parameter name, you can also specify a different one through the use of the [`#lookup_param`](pathname:///api/0.2/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. Finally, the model field that is used to get the model record (defaulting to `pk`) can also be configured by leveraging the [`#lookup_param`](pathname:///api/0.2/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. - -The [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the considered model record. By default, the model record is associated with a `record` key in the template context, but this can also be configured by using the [`record_context_name`](pathname:///api/0.2/Marten/Handlers/RecordDetail.html#record_context_name(name%3AString|Symbol)-class-method) class method. - -## Listing records - -**Class:** [`Marten::Handlers::RecordList`](pathname:///api/0.2/Marten/Handlers/RecordList.html) - -Handler allowing to list model records. - -This base handler can be used to easily expose a list of model records: - -```crystal -class MyHandler < Marten::Handlers::RecordList - template_name "my_template" - model Post -end -``` - -The model class used to retrieve the records can be configured through the use of the [`#model`](pathname:///api/0.2/Marten/Handlers/RecordListing/ClassMethods.html#model(model%3ADB%3A%3AModel.class%3F)-instance-method) class method. The [order](../../models-and-databases/reference/query-set.md#order) of these model records can also be specified by leveraging the [`#ordering`](pathname:///api/0.2/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. - -The [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the list of model records. By default, the list of model records is associated with a `records` key in the template context, but this can also be configured by using the [`list_context_name`](pathname:///api/0.2/Marten/Handlers/RecordList.html#list_context_name(name%3AString|Symbol)-class-method) class method. - -Optionally, it is possible to configure that records should be [paginated](../../models-and-databases/reference/query-set.md#paginator) by specifying a page size through the use of the [`page_size`](pathname:///api/0.2/Marten/Handlers/RecordListing/ClassMethods.html#page_size(page_size%3AInt32%3F)-instance-method) class method: - -```crystal -class MyHandler < Marten::Handlers::RecordList - template_name "my_template" - model Post - page_size 12 -end -``` - -When records are paginated, a [`Marten::DB::Query::Page`](pathname:///api/0.2/Marten/DB/Query/Page.html) object will be exposed in the template context (instead of the raw query set). It should be noted that the page number that should be displayed is determined by looking for a `page` GET parameter by default; this parameter name can be configured as well by calling the [`page_number_param`](pathname:///api/0.2/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. - -:::tip How to customize the query set? -By default, handlers that inherit from [`Marten::Handlers::RecordList`](pathname:///api/0.2/Marten/Handlers/RecordList.html) will use a query set targetting _all_ the records of the specified model. It should be noted that you can customize this behavior easily by overriding the [`#queryset`](pathname:///api/0.2/Marten/Handlers/RecordListing.html#queryset-instance-method) method and by applying additional filters to the default query set. - -For example: - -```crystal -class MyHandler < Marten::Handlers::RecordList - template_name "my_template" - model Article - - def queryset - super.filter(user: request.user) - end -end -``` -::: - -## Updating a record - -**Class:** [`Marten::Handlers::RecordUpdate`](pathname:///api/0.2/Marten/Handlers/RecordUpdate.html) - -Handler allowing to update a model record by processing a schema. - -This handler can be used to process a form, validate its data through the use of a [schema](../../schemas.mdx), and update an existing record by using the validated data. It is expected that the handler will be accessed through a GET request first: when this happens the configured template is rendered and displayed, and the configured schema which is initialized can be accessed from the template context to render a form for example. When the form is submitted via a POST request, the configured schema is validated using the form data. If the data is valid, the model record that was retrieved is updated and the handler returns an HTTP redirect to a configured success URL. - -```crystal -class MyFormHandler < Marten::Handlers::RecordUpdate - model MyModel - schema MyFormSchema - template_name "my_form.html" - success_route_name "my_form_success" -end -``` - -It should be noted that the redirect response issued will be a 302 (found). - -The model class used to update the new record can be configured through the use of the [`#model`](pathname:///api/0.2/Marten/Handlers/RecordRetrieving/ClassMethods.html#model(model%3ADB%3A%3AModel.class%3F)-instance-method) class method. By default, the record to update is retrieved by expecting a `pk` route parameter: this parameter is assumed to contain the value of the primary key field associated with the record that should be updated. If you need to use a different route parameter name, you can also specify a different one through the use of the [`#lookup_param`](pathname:///api/0.2/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. Finally, the model field that is used to get the model record (defaulting to `pk`) can also be configured by leveraging the [`#lookup_param`](pathname:///api/0.2/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. - -The schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.2/Marten/Handlers/Schema.html#schema(schema%3AMarten%3A%3ASchema.class%3F)-class-method) class method. Alternatively, the [`#schema_class`](pathname:///api/0.2/Marten/Handlers/Schema.html#schema_class-instance-method) method can also be overridden to dynamically define the schema class as part of the request handler handling. - -The [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the schema while the [`#success_route_name`](pathname:///api/0.2/Marten/Handlers/Schema.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the schema has been validated. Alternatively, the [`#sucess_url`](pathname:///api/0.2/Marten/Handlers/Schema.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.2/Marten/Handlers/Schema.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. - -## Performing a redirect - -**Class:** [`Marten::Handlers::Redirect`](pathname:///api/0.2/Marten/Handlers/Redirect.html) - -Handler allowing to conveniently return redirect responses. - -This handler can be used to generate a redirect response (temporary or permanent) to another location. To configure such a location, you can either leverage the [`#route_name`](pathname:///api/0.2/Marten/Handlers/Redirect.html#route_name(route_name%3AString%3F)-class-method) class method (which expects a valid [route name](../routing.md#reverse-url-resolutions)) or the [`#url`](pathname:///api/0.2/Marten/Handlers/Redirect.html#url(url%3AString%3F)-class-method) class method. If you need to implement a custom redirection URL logic, you can also override the [`#redirect_url`](pathname:///api/0.2/Marten/Handlers/Redirect.html#redirect_url-instance-method) method. - -```crystal -class TestRedirectHandler < Marten::Handlers::Redirect - route_name "articles:list" -end -``` - -By default, the redirect returned by this handler is a temporary one. In order to generate a permanent redirect response instead, it is possible to leverage the [`#permanent`](pathname:///api/0.2/Marten/Handlers/Redirect.html#permanent(permanent%3ABool)-class-method) class method. - -It should also be noted that by default, incoming query string parameters **are not** forwarded to the redirection URL. If you wish to ensure that these parameters are forwarded, you can make use of the [`forward_query_string`](pathname:///api/0.2/Marten/Handlers/Redirect.html#forward_query_string(forward_query_string%3ABool)-class-method) class method. - -## Processing a schema - -**Class:** [`Marten::Handlers::Schema`](pathname:///api/0.2/Marten/Handlers/Schema.html) - -Handler allowing to process a form through the use of a [schema](../../schemas.mdx). - -This handler can be used to process a form and validate its data through the use of a [schema](../../schemas.mdx). It is expected that the handler will be accessed through a GET request first: when this happens the configured template is rendered and displayed, and the configured schema which is initialized can be accessed from the template context to render a form for example. When the form is submitted via a POST request, the configured schema is validated using the form data. If the data is valid, the handler returns an HTTP redirect to a configured success URL. - -```crystal -class MyFormHandler < Marten::Handlers::Schema - schema MyFormSchema - template_name "my_form.html" - success_route_name "my_form_success" -end -``` - -It should be noted that the redirect response issued will be a 302 (found). - -The schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.2/Marten/Handlers/Schema.html#schema(schema%3AMarten%3A%3ASchema.class%3F)-class-method) class method. Alternatively, the [`#schema_class`](pathname:///api/0.2/Marten/Handlers/Schema.html#schema_class-instance-method) method can also be overridden to dynamically define the schema class as part of the request handler handling. - -The [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the schema while the [`#success_route_name`](pathname:///api/0.2/Marten/Handlers/Schema.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the schema has been validated. Alternatively, the [`#sucess_url`](pathname:///api/0.2/Marten/Handlers/Schema.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.2/Marten/Handlers/Schema.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. - -## Rendering a template - -**Class:** [`Marten::Handlers::Template`](pathname:///api/0.2/Marten/Handlers/Template.html) - -Handler allowing to respond to `GET` request with the content of a rendered HTML [template](../../templates.mdx). - -This handler can be used to render a specific template and returns the resulting content in the response. The template being rendered can be specified by leveraging the [`#template_name`](pathname:///api/0.2/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method. - -```crystal -class HomeHandler < Marten::Handlers::Template - template_name "app/home.html" -end -``` - -If you need to, it is possible to customize the context that is used to render the configured template. To do so, you can define a `#context` method that returns a hash or a named tuple with the values you want to make available to your template: - -```crystal -class HomeHandler < Marten::Handlers::Template - template_name "app/home.html" - - def context - { "recent_articles" => Article.all.order("-published_at")[:5] } - end -end -``` diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/reference/middlewares.md b/docs/versioned_docs/version-0.2/handlers-and-http/reference/middlewares.md deleted file mode 100644 index 110fe459f..000000000 --- a/docs/versioned_docs/version-0.2/handlers-and-http/reference/middlewares.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Middlewares -description: Middlewares reference ---- - -This page provides a reference for all the available [middlewares](../middlewares.md). - -## Flash middleware - -**Class:** [`Marten::Middleware::Flash`](pathname:///api/0.2/Marten/Middleware/Flash.html) - -Enables the use of [flash messages](../introduction.md#using-the-flash-store). - -When this middleware is used, each request will have a flash store initialized and populated from the request's session store. This flash store is a hash-like object that allows to fetch or set values that are associated with specific keys, and that will only be available to the next request (after that they are cleared out). - -The flash store depends on the presence of a working session store. As such, the [Session middleware](#session-middleware) MUST be used along with this middleware. Moreover, this middleware must be placed _after_ the [`Marten::Middleware::Session`](pathname:///api/0.2/Marten/Middleware/Session.html) in the [`middleware`](../../development/reference/settings.md#middleware) setting. - -## GZip middleware - -**Class:** [`Marten::Middleware::GZip`](pathname:///api/0.2/Marten/Middleware/GZip.html) - -Compresses the content of the response if the browser supports GZip compression. - -This middleware will compress responses that are big enough (200 bytes or more) if they don't already contain an Accept-Encoding header. It will also set the Vary header correctly by including Accept-Encoding in it so that caches take into account the fact that the content can be compressed or not. - -The GZip middleware should be positioned before any other middleware that needs to interact with the response content in the [`middleware`](../../development/reference/settings.md#middleware) setting. This is to ensure that the compression happens only when the response content is no longer accessed. - -## I18n middleware - -**Class:** [`Marten::Middleware::I18n`](pathname:///api/0.2/Marten/Middleware/I18n.html) - -Activates the right I18n locale based on incoming requests. - -This middleware will activate the right locale based on the Accept-Language header. Only explicitly-configured locales can be activated by this middleware (that is, locales that are specified in the [`i18n.available_locales`](../../development/reference/settings.md#available_locales) and [`i18n.default_locale`](../../development/reference/settings.md#default_locale) settings). If the incoming locale can't be found in the project configuration, the default locale will be used instead. - -## Session middleware - -**Class:** [`Marten::Middleware::Session`](pathname:///api/0.2/Marten/Middleware/Session.html) - -Enables the use of [sessions](../sessions.md). - -When this middleware is used, each request will have a session store initialized according to the [sessions configuration](../../development/reference/settings.md#sessions-settings). This session store is a hash-like object that allows to fetch or set values that are associated with specific keys. - -The session store is initialized from a session key that is stored as a regular cookie. If the session store ends up being empty after a request's handling, the associated cookie is deleted. Otherwise, the cookie is refreshed if the session store is modified as part of the considered request. Each session cookie is set to expire according to a configured cookie max age (the default cookie max age is 2 weeks). - -## Strict-Transport-Security middleware - -**Class:** [`Marten::Middleware::StrictTransportSecurity`](pathname:///api/0.2/Marten/Middleware/StrictTransportSecurity.html) - -Sets the Strict-Transport-Security header in the response if it wasn't already set. - -This middleware automatically sets the HTTP Strict-Transport-Security (HSTS) response header for all responses unless it was already specified in the response headers. This allows to let browsers know that the considered website should only be accessed using HTTPS, which results in future HTTP requests being automatically converted to HTTPS (up until the configured strict transport policy max age is reached). - -Browsers ensure that this policy is applied for a specific duration because a `max-age` directive is embedded into the header value. This max age duration is expressed in seconds and can be configured using the [`strict_security_policy.max_age`](../../development/reference/settings.md#max_age) setting. - -:::caution -When enabling this middleware, you should probably start with small values for the [`strict_security_policy.max_age`](../../development/reference/settings.md#max_age) setting (for example `3600` - one hour). Indeed, when browsers are aware of the Strict-Transport-Security header they will refuse to connect to your website using HTTP until the expiry time corresponding to the configured max age is reached. - -This is why the value of the [`strict_security_policy.max_age`](../../development/reference/settings.md#max_age) setting is `nil` by default: this prevents the middleware from inserting the Strict-Transport-Security response header until you actually specify a max age. -::: - -## X-Frame-Options middleware - -**Class:** [`Marten::Middleware::XFrameOptions`](pathname:///api/0.2/Marten/Middleware/XFrameOptions.html) - -Sets the X-Frame-Options header in the response if it wasn't already set. - -When this middleware is used, a X-Frame-Options header will be inserted into the HTTP response. The default value for this header (which is configurable via the [`x_frame_options`](../../development/reference/settings.md#x_frame_options) setting) is "DENY", which means that the response cannot be displayed in a frame. This allows preventing click-jacking attacks, by ensuring that the web app cannot be embedded into other sites. - -On the other hand, if the `x_frame_options` is set to "SAMEORIGIN", the page can be displayed in a frame if the including site is the same as the one serving the page. diff --git a/docs/versioned_docs/version-0.3/caching/how-to/create-custom-cache-stores.md b/docs/versioned_docs/version-0.3/caching/how-to/create-custom-cache-stores.md index 8bbefb9a1..3e3fbb05f 100644 --- a/docs/versioned_docs/version-0.3/caching/how-to/create-custom-cache-stores.md +++ b/docs/versioned_docs/version-0.3/caching/how-to/create-custom-cache-stores.md @@ -138,7 +138,7 @@ end ## Enabling the use of custom cache stores -Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache-store1) setting. +Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache_store) setting. For example: diff --git a/docs/versioned_docs/version-0.3/development/applications.md b/docs/versioned_docs/version-0.3/development/applications.md index 6277d8985..fb5ccb14f 100644 --- a/docs/versioned_docs/version-0.3/development/applications.md +++ b/docs/versioned_docs/version-0.3/development/applications.md @@ -16,7 +16,7 @@ Another benefit of applications is that they can be packaged and reused across m ## Using applications -The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installedapps) setting. +The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installed_apps) setting. This setting corresponds to an array of installed app classes. Indeed, each Marten application must define a subclass of [`Marten::App`](pathname:///api/0.3/Marten/App.html) to specify a few things such as the application label (see [Creating applications](#creating-applications) for more information about this). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. @@ -40,7 +40,7 @@ Adding an application class inside this array will have the following impact on ### The main application -The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installedapps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. +The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installed_apps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. :::info The main application is associated with the `main` label. This means that models of the main application that do not define an explicit table name will have table names starting with `main_`. @@ -52,7 +52,7 @@ In the end, the main application provides a convenient way for starting projects ### Order of installed applications -You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installedapps) setting can actually matter. +You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installed_apps) setting can actually matter. For example, a "foo" app might define a `test.html` template, and a similar template with the exact same name might be defined by a "bar" app. If the "foo" app appears before the "bar" app in the array of installed apps, then requesting and rendering the `test.html` template will actually involve the "foo" app's template only. This is because template loaders associated with app directories iterate over applications in the order in which they are defined in the installed apps array. diff --git a/docs/versioned_docs/version-0.3/models-and-databases/how-to/create-custom-model-fields.md b/docs/versioned_docs/version-0.3/models-and-databases/how-to/create-custom-model-fields.md index f8b6a5650..16a2b9e57 100644 --- a/docs/versioned_docs/version-0.3/models-and-databases/how-to/create-custom-model-fields.md +++ b/docs/versioned_docs/version-0.3/models-and-databases/how-to/create-custom-model-fields.md @@ -137,7 +137,7 @@ def from_db_result_set(result_set : ::DB::ResultSet) : ::UUID? end ``` -The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#fromdb) once you get the value from the database result set in order to return the final value. +The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#from_db) once you get the value from the database result set in order to return the final value. #### `to_column` @@ -163,7 +163,7 @@ If for some reason your custom field does not contribute any columns to the data #### `to_db` -The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#fromdb) method. +The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#from_db) method. For example, this method could return the string representation of a `UUID` object: diff --git a/docs/versioned_docs/version-0.3/models-and-databases/transactions.md b/docs/versioned_docs/version-0.3/models-and-databases/transactions.md index 7932ec813..eea12318d 100644 --- a/docs/versioned_docs/version-0.3/models-and-databases/transactions.md +++ b/docs/versioned_docs/version-0.3/models-and-databases/transactions.md @@ -43,7 +43,7 @@ When transaction blocks are nested, this results in all the database statements Basic model operations such as [creating](./introduction.md#create), [updating](./introduction.md#update), or [deleting](./introduction.md#delete) records are automatically wrapped in a transaction. This helps in ensuring that any exception that is raised in the context of validations or as part of `after_*` [callbacks](./callbacks.md) (ie. `after_create`, `after_update`, `after_save`, and `after_delete`) will also roll back the current transaction. -The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#aftercommit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). +The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#after_commit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). ## Exception handling and rollbacks diff --git a/docs/versioned_docs/version-0.3/templates/how-to/create-custom-context-producers.md b/docs/versioned_docs/version-0.3/templates/how-to/create-custom-context-producers.md index 18de1d474..e21684b79 100644 --- a/docs/versioned_docs/version-0.3/templates/how-to/create-custom-context-producers.md +++ b/docs/versioned_docs/version-0.3/templates/how-to/create-custom-context-producers.md @@ -25,4 +25,4 @@ end ## Activating context producers -As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#contextproducers) setting in order to be used by the Marten templates engine when initializing new context objects. +As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#context_producers) setting in order to be used by the Marten templates engine when initializing new context objects. diff --git a/docs/versioned_docs/version-0.3/templates/introduction.md b/docs/versioned_docs/version-0.3/templates/introduction.md index 60dff026c..f7da22ef4 100644 --- a/docs/versioned_docs/version-0.3/templates/introduction.md +++ b/docs/versioned_docs/version-0.3/templates/introduction.md @@ -183,7 +183,7 @@ It's important to remember that the `super` template tag can only be used withi Templates can be loaded from specific locations within your codebase and from application folders. This is controlled by two main settings: * [`templates.app_dirs`](../development/reference/settings.md#app_dirs-1) is a boolean that indicates whether or not it should be possible to load templates that are provided by [installed applications](../development/reference/settings.md#installed_apps). Indeed, applications can define a `templates` folder at their root, and these templates will be discoverable by Marten if this setting is set to `true` -* [`templates.dirs`](../development/reference/settings.md#dirs1) is an array of additional directories where templates should be looked for +* [`templates.dirs`](../development/reference/settings.md#dirs-1) is an array of additional directories where templates should be looked for Application templates are always enabled by default (`templates.app_dirs = true`) for new Marten projects. @@ -303,7 +303,7 @@ Context producers are helpers that ensure that common variables are automaticall For example, they can be used to insert the current HTTP request object in every template context being rendered in the context of a handler and HTTP request. This makes sense considering that the HTTP request object is a common object that is likely to be used by multiple templates in your project: that way there is no need to explicitly "insert" it in the context every time you render a template. This specific capability is provided by the [`Marten::Template::ContextProducer::Request`](pathname:///api/0.3/Marten/Template/ContextProducer/Request.html) context producer, which inserts a `request` object into every template context. -Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#contextproducers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: +Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#context_producers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: ```crystal config.templates.context_producers = [ diff --git a/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.2.md b/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.2.md index 039c9b614..434c74ee1 100644 --- a/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.2.md +++ b/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.2.md @@ -73,7 +73,7 @@ end ### Transaction callbacks -Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#aftercommit) and [`#after_rollback`](../../models-and-databases/callbacks.md#afterrollback) macros. +Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#after_commit) and [`#after_rollback`](../../models-and-databases/callbacks.md#after_rollback) macros. For example: diff --git a/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.3.md b/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.3.md index 7bedfef92..3bb211034 100644 --- a/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.3.md +++ b/docs/versioned_docs/version-0.3/the-marten-project/release-notes/0.3.md @@ -127,7 +127,7 @@ end #### Development -* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowedhosts) setting). +* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) setting). * [`Marten#setup`](pathname:///api/0.3/Marten.html#setup-class-method) now raises.[`Marten::Conf::Errors::InvalidConfiguration`](pathname:///api/0.3/Marten/Conf/Errors/InvalidConfiguration.html) exceptions when a configured database involves a backend that is not installed (eg. a MySQL database configured without `crystal-lang/crystal-mysql` installed and required). * The [`new`](../../development/reference/management-commands.md#new) management command now automatically creates a [`.editorconfig`](https://editorconfig.org) file for new projects. * A new [`root_path`](../../development/reference/settings.md#root_path) setting was introduced to make it possible to configure the actual location of the project sources in your system. This is especially useful when deploying projects that have been compiled in a different location from their final destination, which can happen on platforms like Heroku. By setting the root path, you can ensure that your application can find all necessary project sources, as well as other files like locales, assets, and templates. diff --git a/docs/versioned_docs/version-0.4/caching/how-to/create-custom-cache-stores.md b/docs/versioned_docs/version-0.4/caching/how-to/create-custom-cache-stores.md index aa48a15ec..b49081d71 100644 --- a/docs/versioned_docs/version-0.4/caching/how-to/create-custom-cache-stores.md +++ b/docs/versioned_docs/version-0.4/caching/how-to/create-custom-cache-stores.md @@ -138,7 +138,7 @@ end ## Enabling the use of custom cache stores -Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache-store1) setting. +Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache_store) setting. For example: diff --git a/docs/versioned_docs/version-0.4/development/applications.md b/docs/versioned_docs/version-0.4/development/applications.md index 956038311..c278887c4 100644 --- a/docs/versioned_docs/version-0.4/development/applications.md +++ b/docs/versioned_docs/version-0.4/development/applications.md @@ -16,7 +16,7 @@ Another benefit of applications is that they can be packaged and reused across m ## Using applications -The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installedapps) setting. +The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installed_apps) setting. This setting corresponds to an array of installed app classes. Indeed, each Marten application must define a subclass of [`Marten::App`](pathname:///api/0.4/Marten/App.html) to specify a few things such as the application label (see [Creating applications](#creating-applications) for more information about this). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. @@ -40,7 +40,7 @@ Adding an application class inside this array will have the following impact on ### The main application -The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installedapps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. +The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installed_apps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. :::info The main application is associated with the `main` label. This means that models of the main application that do not define an explicit table name will have table names starting with `main_`. @@ -52,7 +52,7 @@ In the end, the main application provides a convenient way for starting projects ### Order of installed applications -You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installedapps) setting can actually matter. +You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installed_apps) setting can actually matter. For example, a "foo" app might define a `test.html` template, and a similar template with the exact same name might be defined by a "bar" app. If the "foo" app appears before the "bar" app in the array of installed apps, then requesting and rendering the `test.html` template will actually involve the "foo" app's template only. This is because template loaders associated with app directories iterate over applications in the order in which they are defined in the installed apps array. diff --git a/docs/versioned_docs/version-0.4/models-and-databases/how-to/create-custom-model-fields.md b/docs/versioned_docs/version-0.4/models-and-databases/how-to/create-custom-model-fields.md index baac79533..4b0c0f17b 100644 --- a/docs/versioned_docs/version-0.4/models-and-databases/how-to/create-custom-model-fields.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/how-to/create-custom-model-fields.md @@ -137,7 +137,7 @@ def from_db_result_set(result_set : ::DB::ResultSet) : ::UUID? end ``` -The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#fromdb) once you get the value from the database result set in order to return the final value. +The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#from_db) once you get the value from the database result set in order to return the final value. #### `to_column` @@ -163,7 +163,7 @@ If for some reason your custom field does not contribute any columns to the data #### `to_db` -The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#fromdb) method. +The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#from_db) method. For example, this method could return the string representation of a `UUID` object: diff --git a/docs/versioned_docs/version-0.4/models-and-databases/introduction.md b/docs/versioned_docs/version-0.4/models-and-databases/introduction.md index 7140d81a7..de40c1543 100644 --- a/docs/versioned_docs/version-0.4/models-and-databases/introduction.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/introduction.md @@ -405,7 +405,7 @@ Please head over to the [Model validations](./validations.md) guide in order to Model classes can inherit from each other. This allows you to easily reuse the field definitions and table attributes of a parent model within a child model. -Presently, the Marten web framework allows [abstract model inheritance](#inheriting-from-abstract-models) (which is useful in order to reuse shared model fields and patterns over multiple child models without having a database table created for the parent model) and [multi-table inheritance](#multi-table-inheritance). +Presently, the Marten web framework allows [abstract model inheritance](#abstract-model-inheritance) (which is useful in order to reuse shared model fields and patterns over multiple child models without having a database table created for the parent model) and [multi-table inheritance](#multi-table-inheritance). ### Abstract model inheritance diff --git a/docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md b/docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md index 253325557..8dec22aee 100644 --- a/docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md @@ -93,7 +93,7 @@ query_set_1 = Post.all.distinct query_set_2 = Post.all.distinct(:title) ``` -It should be noted that it is also possible to follow associations of direct related models too by using the [double underscores notation](../queries.md#joins-and-filtering-relations) (`__`). For example the following query will select distinct records based on a joined "author" attribute: +It should be noted that it is also possible to follow associations of direct related models too by using the [double underscores notation](../queries.md#filtering-relations) (`__`). For example the following query will select distinct records based on a joined "author" attribute: ``` query_set = Post.all.distinct(:author__name) @@ -141,7 +141,7 @@ query_set.filter { (q(name: "Foo") | q(name: "Bar")) & q(is_published: True) } ### `join` -Returns a queryset whose specified `relations` are "followed" and joined to each result (see [Queries](../queries.md#joins-and-filtering-relations) for an introduction about this capability). +Returns a queryset whose specified `relations` are "followed" and joined to each result (see [Queries](../queries.md#filtering-relations) for an introduction about this capability). When using `#join`, the specified relationships will be followed and each record returned by the queryset will have the corresponding related objects already selected and populated. Using `#join` can result in performance improvements since it can help reduce the number of SQL queries, as illustrated by the following example: diff --git a/docs/versioned_docs/version-0.4/models-and-databases/transactions.md b/docs/versioned_docs/version-0.4/models-and-databases/transactions.md index 2a292ed2a..b7168a90d 100644 --- a/docs/versioned_docs/version-0.4/models-and-databases/transactions.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/transactions.md @@ -43,7 +43,7 @@ When transaction blocks are nested, this results in all the database statements Basic model operations such as [creating](./introduction.md#create), [updating](./introduction.md#update), or [deleting](./introduction.md#delete) records are automatically wrapped in a transaction. This helps in ensuring that any exception that is raised in the context of validations or as part of `after_*` [callbacks](./callbacks.md) (ie. `after_create`, `after_update`, `after_save`, and `after_delete`) will also roll back the current transaction. -The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#aftercommit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). +The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#after_commit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). ## Exception handling and rollbacks diff --git a/docs/versioned_docs/version-0.4/templates/how-to/create-custom-context-producers.md b/docs/versioned_docs/version-0.4/templates/how-to/create-custom-context-producers.md index 779c3a6fd..61048c5ca 100644 --- a/docs/versioned_docs/version-0.4/templates/how-to/create-custom-context-producers.md +++ b/docs/versioned_docs/version-0.4/templates/how-to/create-custom-context-producers.md @@ -25,4 +25,4 @@ end ## Activating context producers -As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#contextproducers) setting in order to be used by the Marten templates engine when initializing new context objects. +As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#context_producers) setting in order to be used by the Marten templates engine when initializing new context objects. diff --git a/docs/versioned_docs/version-0.4/templates/introduction.md b/docs/versioned_docs/version-0.4/templates/introduction.md index edb3aaa80..e462b5a35 100644 --- a/docs/versioned_docs/version-0.4/templates/introduction.md +++ b/docs/versioned_docs/version-0.4/templates/introduction.md @@ -212,7 +212,7 @@ Obviously, it is also possible to include partials that don't require any variab Templates can be loaded from specific locations within your codebase and from application folders. This is controlled by two main settings: * [`templates.app_dirs`](../development/reference/settings.md#app_dirs-1) is a boolean that indicates whether or not it should be possible to load templates that are provided by [installed applications](../development/reference/settings.md#installed_apps). Indeed, applications can define a `templates` folder at their root, and these templates will be discoverable by Marten if this setting is set to `true` -* [`templates.dirs`](../development/reference/settings.md#dirs1) is an array of additional directories where templates should be looked for +* [`templates.dirs`](../development/reference/settings.md#dirs-1) is an array of additional directories where templates should be looked for Application templates are always enabled by default (`templates.app_dirs = true`) for new Marten projects. @@ -332,7 +332,7 @@ Context producers are helpers that ensure that common variables are automaticall For example, they can be used to insert the current HTTP request object in every template context being rendered in the context of a handler and HTTP request. This makes sense considering that the HTTP request object is a common object that is likely to be used by multiple templates in your project: that way there is no need to explicitly "insert" it in the context every time you render a template. This specific capability is provided by the [`Marten::Template::ContextProducer::Request`](pathname:///api/0.4/Marten/Template/ContextProducer/Request.html) context producer, which inserts a `request` object into every template context. -Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#contextproducers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: +Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#context_producers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: ```crystal config.templates.context_producers = [ diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.md index 039c9b614..434c74ee1 100644 --- a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.md +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.md @@ -73,7 +73,7 @@ end ### Transaction callbacks -Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#aftercommit) and [`#after_rollback`](../../models-and-databases/callbacks.md#afterrollback) macros. +Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#after_commit) and [`#after_rollback`](../../models-and-databases/callbacks.md#after_rollback) macros. For example: diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.md index 7bedfef92..3bb211034 100644 --- a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.md +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.md @@ -127,7 +127,7 @@ end #### Development -* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowedhosts) setting). +* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) setting). * [`Marten#setup`](pathname:///api/0.3/Marten.html#setup-class-method) now raises.[`Marten::Conf::Errors::InvalidConfiguration`](pathname:///api/0.3/Marten/Conf/Errors/InvalidConfiguration.html) exceptions when a configured database involves a backend that is not installed (eg. a MySQL database configured without `crystal-lang/crystal-mysql` installed and required). * The [`new`](../../development/reference/management-commands.md#new) management command now automatically creates a [`.editorconfig`](https://editorconfig.org) file for new projects. * A new [`root_path`](../../development/reference/settings.md#root_path) setting was introduced to make it possible to configure the actual location of the project sources in your system. This is especially useful when deploying projects that have been compiled in a different location from their final destination, which can happen on platforms like Heroku. By setting the root path, you can ensure that your application can find all necessary project sources, as well as other files like locales, assets, and templates. diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.4.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.4.md index 79894a573..434e30518 100644 --- a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.4.md +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.4.md @@ -134,7 +134,7 @@ end * The hash of matched routing parameters that is available from handlers through the use of the `#params` method can accept symbols and strings when performing key lookups. * The [GZip middleware](../../handlers-and-http/reference/middlewares.md#gzip-middleware) now incorporates a mitigation strategy against the BREACH attack. This strategy (described in the [Heal The Breach paper](https://ieeexplore.ieee.org/document/9754554)) involves introducing up to 100 random bytes into GZip responses to enhance the security against such attacks. * A new [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback type is now available to handlers. Such callbacks are executed before rendering a [template](../../templates/introduction.md) in order to produce a response. As such they are well suited for adding new variables to the global template context so that they are available to the template runtime. -* All handlers now have access to a [global template context](../../handlers-and-http/introduction.md#global-template-context) through the use of the [`#context`](pathname:///api/0.4/Marten/Handlers/Base.html#context-instance-method) method. This template context object is available for the lifetime of the considered handler and can be mutated to define which variables are made available to the template runtime when rendering templates (either through the use of the [`#render`](#render) helper method or when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/generic-handlers.md#rendering-a-template) generic handler). This feature can be combined with the [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback to effortlessly introduce new variables to the context used for rendering a template and generating a handler response. +* All handlers now have access to a [global template context](../../handlers-and-http/introduction.md#global-template-context) through the use of the [`#context`](pathname:///api/0.4/Marten/Handlers/Base.html#context-instance-method) method. This template context object is available for the lifetime of the considered handler and can be mutated to define which variables are made available to the template runtime when rendering templates (either through the use of the [`#render`](../../handlers-and-http/introduction.md#render) helper method or when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/generic-handlers.md#rendering-a-template) generic handler). This feature can be combined with the [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback to effortlessly introduce new variables to the context used for rendering a template and generating a handler response. #### Templates diff --git a/docs/versioned_docs/version-0.2/assets.mdx b/docs/versioned_docs/version-0.5/assets.mdx similarity index 100% rename from docs/versioned_docs/version-0.2/assets.mdx rename to docs/versioned_docs/version-0.5/assets.mdx diff --git a/docs/versioned_docs/version-0.2/assets/introduction.md b/docs/versioned_docs/version-0.5/assets/introduction.md similarity index 73% rename from docs/versioned_docs/version-0.2/assets/introduction.md rename to docs/versioned_docs/version-0.5/assets/introduction.md index 81ca98fc2..90d3253d2 100644 --- a/docs/versioned_docs/version-0.2/assets/introduction.md +++ b/docs/versioned_docs/version-0.5/assets/introduction.md @@ -23,7 +23,7 @@ The assets flow provided by Marten is **intentionally simple**. Indeed, Marten b Once assets have been "collected", it is possible to generate their URLs through the use of dedicated helpers: -* by using the [assets engine](pathname:///api/0.2/Marten/Asset/Engine.html#url(filepath%3AString)%3AString-instance-method) in Crystal +* by using the [assets engine](pathname:///api/0.5/Marten/Asset/Engine.html#url(filepath%3AString)%3AString-instance-method) in Crystal * by using the [`asset`](../templates/reference/tags.md#asset) tag in templates The way these asset URLs are generated depends on the configured [asset storage](../development/reference/settings.md#storage). @@ -41,7 +41,7 @@ config.assets.url = "/assets/" ### Assets storage -One of the most important asset settings is the [`storage`](../development/reference/settings.md#storage) one. Indeed, Marten uses a file storage mechanism to perform file operations related to assets (like uploading files, generating URLs, etc) by leveraging a standardized API. By default, assets use the [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage backend, which ensures that assets files are collected and placed to a specific folder in the local file system: this allows these files to then be served by a web server such as Nginx for example. +One of the most important asset settings is the [`storage`](../development/reference/settings.md#storage) one. Indeed, Marten uses a file storage mechanism to perform file operations related to assets (like uploading files, generating URLs, etc) by leveraging a standardized API. By default, assets use the [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage backend, which ensures that assets files are collected and placed to a specific folder in the local file system: this allows these files to then be served by a web server such as Nginx for example. ### Assets root directory @@ -49,7 +49,7 @@ This directory - which can be configured through the use of the [`root`](../deve ### Assets URL -The asset URL is used when generating URLs for assets. This base URL will be used by the default [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage to construct asset URLs. For example, requesting a `css/App.css` asset might generate a `/assets/css/App.css` URL. The default value is `/assets/`. +The asset URL is used when generating URLs for assets. This base URL will be used by the default [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage to construct asset URLs. For example, requesting a `css/App.css` asset might generate a `/assets/css/App.css` URL. The default value is `/assets/`. ### Asset directories @@ -64,6 +64,41 @@ config.assets.dirs = [ ] ``` +### Asset manifests and fingerprinting + +Fingerprinting involves adding a unique string of characters to the filename of each asset. This enables the browser to cache the file securely. When an asset is modified, its fingerprint changes, prompting the browser to retrieve and use the updated version. + +Modern asset bundling tools often provide the capability to generate manifest files. These manifest files typically contain mappings between the original asset filenames and their corresponding fingerprinted versions. Marten supports configuring paths to these manifest files so that [resolving assets](#resolving-asset-urls) produces URLs that automatically include the correct fingerprinted version of each asset. + +This can be achieved by adding manifest paths to the [`assets.manifests`](../development/reference/settings.md#manifests) setting. For example: + +```crystal +config.assets.manifests = [ + "src/assets/build/manifest.json", +] +``` + +It is assumed that the files whose paths are referenced in this setting are regular JSON manifests, containing mappings between original asset file names and their fingerprinted versions. For example: + +```json +{ + "app/home.css": "app/home.9495841be78cdf06c45d.css", + "app/home.js": "app/home.9495841be78cdf06c45d.js" +} +``` + +Considering the above manifest example, trying to resolve `app/home.css` would produce a URL ending with `app/home.9495841be78cdf06c45d.css`: + +```crystal +Marten.assets.url("app/home.css") # => "/assets/app/home.9495841be78cdf06c45d.css" +``` + +:::info +Additionally, the [`collectassets`](../development/reference/management-commands.md#collectassets) command provides a `--fingerprint` option. Using this option automatically fingerprints the collected assets and generates a `manifest.json` file, which maps the original file paths to their fingerprinted versions. + +When the `--fingerprint` option is used, it's important to include the path to the generated "manifest.json" in the appropriate [`assets.manifests`](../development/reference/settings#manifests) environment config file, otherwise the collected assets can't be found when the URL is resolved. +::: + ## Resolving asset URLs As mentioned previously, assets are collected and persisted in a specific storage. When building HTML [templates](../templates/introduction.md), you will usually need to "resolve" the URL of assets to generate the absolute URLs that should be inserted into stylesheet or script tags (for example). @@ -78,7 +113,7 @@ For example: In the above snippet, the `app/app.css` asset could be resolved to `/assets/app/app.css` (depending on the configuration of the project obviously). -It is also possible to resolve asset URLs programmatically in Crystal. To do so, you can leverage the [`#url`](pathname:///api/0.2/Marten/Asset/Engine.html#url(filepath%3AString)%3AString-instance-method) method of the Marten assets engine: +It is also possible to resolve asset URLs programmatically in Crystal. To do so, you can leverage the [`#url`](pathname:///api/0.5/Marten/Asset/Engine.html#url(filepath%3AString)%3AString-instance-method) method of the Marten assets engine: ```crystal Marten.assets.url("app/app.css") # => "/assets/app/app.css" @@ -86,7 +121,7 @@ Marten.assets.url("app/app.css") # => "/assets/app/app.css" ## Serving assets in development -Marten provides a handler that you can use to serve assets in development environments only. This handler ([`Marten::Handlers::Defaults::Development::ServeAsset`](pathname:///api/0.2/Marten/Handlers/Defaults/Development/ServeAsset.html)) is automatically mapped to a route when creating new projects through the use of the [`new`](../development/reference/management-commands.md#new) management command: +Marten provides a handler that you can use to serve assets in development environments only. This handler ([`Marten::Handlers::Defaults::Development::ServeAsset`](pathname:///api/0.5/Marten/Handlers/Defaults/Development/ServeAsset.html)) is automatically mapped to a route when creating new projects through the use of the [`new`](../development/reference/management-commands.md#new) management command: ```crystal Marten.routes.draw do @@ -101,7 +136,7 @@ end As you can see, this route will automatically use the URL that is configured as part of the [`url`](../development/reference/settings.md#url) asset setting. For example, this means that an `app/app.css` asset would be served by the `/assets/app/app.css` route in development if the [`url`](../development/reference/settings.md#url) setting is set to `/assets/`. :::warning -It is very important to understand that this handler should **only** be used in development environments. Indeed, the [`Marten::Handlers::Defaults::Development::ServeAsset`](pathname:///api/0.2/Marten/Handlers/Defaults/Development/ServeAsset.html) handler does not require assets to have been collected beforehand through the use of the [`collectassets`](../development/reference/management-commands.md#collectassets) management command. This means that it will try to find assets in your applications' `assets` directories and in the directories configured in the [`dirs`](../development/reference/settings.md#dirs) setting. This mechanism is helpful in development, but it is not suitable for production environments since it is inneficient and (probably) insecure. +It is very important to understand that this handler should **only** be used in development environments. Indeed, the [`Marten::Handlers::Defaults::Development::ServeAsset`](pathname:///api/0.5/Marten/Handlers/Defaults/Development/ServeAsset.html) handler does not require assets to have been collected beforehand through the use of the [`collectassets`](../development/reference/management-commands.md#collectassets) management command. This means that it will try to find assets in your applications' `assets` directories and in the directories configured in the [`dirs`](../development/reference/settings.md#dirs) setting. This mechanism is helpful in development, but it is not suitable for production environments since it is ineficient and (probably) insecure. ::: ## Serving assets in production @@ -118,7 +153,7 @@ It should be noted that there are many ways to serve assets in production. Again ### Serving assets from a web server -As mentioned previously, Marten uses a file storage mechanism to perform file operations related to assets and to "collect" them. By default, assets use the [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage backend, which ensures that assets files are collected and placed into a specific folder in the local file system. This allows these assets to easily be served by a local web server if you have one properly configured. +As mentioned previously, Marten uses a file storage mechanism to perform file operations related to assets and to "collect" them. By default, assets use the [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage backend, which ensures that assets files are collected and placed into a specific folder in the local file system. This allows these assets to easily be served by a local web server if you have one properly configured. For example, you could use a web server like [Apache](https://httpd.apache.org/) or [Nginx](https://nginx.org) to serve your collected assets. The way to configure these web servers will obviously vary from one solution to another, but you will likely need to define a location whose URL matches the [`url`](../development/reference/settings.md#url) setting value and that serves files from the folder where assets were collected (the [`root`](../development/reference/settings.md#root) folder). @@ -157,4 +192,22 @@ To serve assets from a cloud storage (like Amazon's S3 or GCS) and (optionally) Marten does not provide file storage implementations for the most frequently encountered cloud storage solutions presently. This is something that is planned for future releases though. ::: -Writing a custom file storage implementation will involve subclassing the [`Marten::Core::Storage::Base`](pathname:///api/0.2/Marten/Core/Storage/Base.html) abstract class and implementing a set of mandatory methods. The main difference compared to a "local file system" storage here is that you would need to make use of the API of the chosen cloud storage to perform low-level file operations (such as reading a file's content, verifying that a file exists, or generating a file URL). +Writing a custom file storage implementation will involve subclassing the [`Marten::Core::Storage::Base`](pathname:///api/0.5/Marten/Core/Storage/Base.html) abstract class and implementing a set of mandatory methods. The main difference compared to a "local file system" storage here is that you would need to make use of the API of the chosen cloud storage to perform low-level file operations (such as reading a file's content, verifying that a file exists, or generating a file URL). + +### Serving assets using a middleware + +There are some situations where it is not possible to easily configure a web server such as [Nginx](https://nginx.org) or a third-party service (like Amazon's S3 or GCS) to serve your assets directly. To palliate this, Marten provides the [`Marten::Middleware::AssetServing`](../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware. + +The purpose of this middleware is to distribute collected assets stored under the configured assets root ([`assets.root`](../development/reference/settings.md#root) setting). These assets are assumed to have been collected using the [`collectassets`](../development/reference/management-commands.md#collectassets) management command, and it is also assumed that a "local file system" storage (such as [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html)) is used. + +In order to use this middleware, you can "insert" the corresponding class at the beginning of the [`middleware`](../development/reference/settings.md#middleware) setting when defining production settings. For example: + +```crystal +Marten.configure :production do |config| + config.middleware.unshift(Marten::Middleware::AssetServing) + + # Other settings... +end +``` + +It is important to note that the [`assets.url`](../development/reference/settings.md#url) setting must align with the Marten application domain or correspond to a relative URL path (e.g., /assets/) for this middleware to work correctly. This guarantees proper mapping and accessibility of the assets within the application, allowing them to be served by this middleware. diff --git a/docs/versioned_docs/version-0.2/authentication.mdx b/docs/versioned_docs/version-0.5/authentication.mdx similarity index 100% rename from docs/versioned_docs/version-0.2/authentication.mdx rename to docs/versioned_docs/version-0.5/authentication.mdx diff --git a/docs/versioned_docs/version-0.2/authentication/introduction.md b/docs/versioned_docs/version-0.5/authentication/introduction.md similarity index 69% rename from docs/versioned_docs/version-0.2/authentication/introduction.md rename to docs/versioned_docs/version-0.5/authentication/introduction.md index e97c067f0..ddf3ad3cb 100644 --- a/docs/versioned_docs/version-0.2/authentication/introduction.md +++ b/docs/versioned_docs/version-0.5/authentication/introduction.md @@ -8,7 +8,9 @@ Marten allows the generation of new projects with a built-in authentication appl ## Overview -Marten's [`new`](../development/reference/management-commands.md#new) management command allows the generation of projects with a built-in `auth` application. This application is part of the created project: it provides the necessary [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [schemas](../schemas.mdx), [emails](../emailing.mdx), and [templates](../templates.mdx) allowing to authenticate users with email addresses and passwords, while also supporting standard password reset flows. On top of that, an `Auth::User` model is automatically generated for your newly created projects. Since this model is also part of your project, this means that it's possible to easily add new fields to it and generate migrations for it as well. +Marten's [`new`](../development/reference/management-commands.md#new) management command allows the generation of projects with a built-in `auth` application. The same application can also be added to existing projects by leveraging the [`auth`](../development/reference/generators.md#auth) generator. + +The generated authentication application is part of your project: it provides the necessary [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [schemas](../schemas.mdx), [emails](../emailing.mdx), and [templates](../templates.mdx) allowing to authenticate users with email addresses and passwords, while also supporting standard password reset flows. On top of that, an `Auth::User` model is automatically generated for your newly created projects. Since this model is also part of your project, this means that it's possible to easily add new fields to it and generate migrations for it as well. Here is the list of responsibilities of the generated authentication application: @@ -30,7 +32,7 @@ For example: marten new project myblog --with-auth ``` -When using this option, Marten will generate an `auth` [application](../development/applications.md) under the `src/auth` folder of your project. As mentioned previously, this application provides a set of [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [schemas](../schemas.mdx), [emails](../emailing.mdx), and [templates](../templates.mdx) that implement basic authentication operations. +When using this option, Marten will generate an `auth` [application](../development/applications.md) under the `src/apps/auth` folder of your project. As mentioned previously, this application provides a set of [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [schemas](../schemas.mdx), [emails](../emailing.mdx), and [templates](../templates.mdx) that implement basic authentication operations. You can test the generated authentication application by going to your application at [http://localhost:8000/auth/signup](http://localhost:8000/auth/signup) after having started the Marten development server (using `marten serve`). @@ -38,6 +40,36 @@ You can test the generated authentication application by going to your applicati You can see the full list of files generated for the `auth` application in [Generated files](./reference/generated-files.md). ::: +## Adding authentication to existing projects + +The [`auth`](../development/reference/generators.md#auth) generator can be leveraged in order to add an authentication application to an existing project. + +For example, the following command will add a new authentication app with the `auth` label to the current project: + +```bash +marten gen auth +``` + +:::tip +Note that you can also customize the label given to the generated authentication app by providing an additional argument containing the intended app label: + +```bash +marten gen auth my_auth +``` +::: + +This generator will add an authentication application under your project's `src` folder (or `src/apps` folder if it is defined). As mentioned previously, this application provides a set of [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [schemas](../schemas.mdx), [emails](../emailing.mdx), and [templates](../templates.mdx) that implement basic authentication operations. + +Note that the generator will also add the generated application to the [`installed_apps`](../development/reference/settings.md#installed_apps) setting and will also configure Crystal requirements for it (in the `src/project.cr` and `src/cli.cr` files). It will also add authentication-related settings to your base settings file and will add the [`marten-auth`](https://github.com/martenframework/marten-auth) shard to your project's `shard.yml` automatically. + +:::info +You can see the full list of files generated for the generated authentication application in [Generated files](./reference/generated-files.md). +::: + +:::tip +Don't forget to run [`marten migrate`](../development/reference/management-commands.md#migrate) after the authentication app has been generated so that your user model gets created at the database level. You should also check the `config/routes.cr` file or run the [`marten routes`](../development/reference/management-commands.md#routes) management command to see the routes associated with your generated authentication app. +::: + ## Usage This section covers the basics of how to use the `auth` application - powered by [`marten-auth`](https://github.com/martenframework/marten-auth) - that is generated when creating projects with the `--with-auth` option. @@ -54,7 +86,7 @@ The `auth` application defines a single `Auth::User` model that inherits its fie ### Retrieving the current user -Projects that are generated with the `auth` application automatically make use of a middleware (`MartenAuth::Middleware`) that ensures that the currently authenticated user ID is associated with the current request. This means that given a specific HTTP request (instance of [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html)), it is possible to identify which user is signed-in or not. Concretely, the following methods are made available on the standard [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html) object in order to interact with the currently signed-in user: +Projects that are generated with the `auth` application automatically make use of a middleware (`MartenAuth::Middleware`) that ensures that the currently authenticated user ID is associated with the current request. This means that given a specific HTTP request (instance of [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html)), it is possible to identify which user is signed-in or not. Concretely, the following methods are made available on the standard [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html) object in order to interact with the currently signed-in user: | Method | Description | | --- | --- | @@ -91,6 +123,20 @@ end user.save! ``` +:::tip +In certain scenarios where passwords aren't required, such as with email magic link authentication or multi-provider OAuth authentication, it may be desirable to create user accounts without passwords. To achieve this, you can utilize the `#set_unusable_password` method on your user model instances. This method ensures that no password can be used for these accounts. + +For example: + +```crystal +user = Auth::User.new(email: "test@example.com") do |user| + user.set_unusable_password +end + +user.save! +``` +::: + ### Authenticating users Authentication is the act of verifying a user's credentials. This capability is provided by the [`marten-auth`](https://github.com/martenframework/marten-auth) shard through the use of the `MartenAuth#authenticate` method: this method tries to authenticate the user associated identified by a natural key (typically, an email address) and check that the given raw password is valid. The method returns the corresponding user record if the authentication is successful. Otherwise, it returns `nil` if the credentials can't be verified because the user does not exist or because the password is invalid. @@ -116,7 +162,7 @@ The `MartenAuth#authenticate` method is automatically used by the handlers that ### Signing in users -Signing in a user is the act of attaching it to the current session - after having verified that the associated credentials are valid (see [Authenticating users](#authenticating-users)). This capability is provided by the [`marten-auth`](https://github.com/martenframework/marten-auth) shard through the use of the `MartenAuth#sign_in` method: This method takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html)) and a user record as arguments and ensures that the user ID is attached to the current session so that they do not have to reauthenticate for every request. +Signing in a user is the act of attaching it to the current session - after having verified that the associated credentials are valid (see [Authenticating users](#authenticating-users)). This capability is provided by the [`marten-auth`](https://github.com/martenframework/marten-auth) shard through the use of the `MartenAuth#sign_in` method: This method takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html)) and a user record as arguments and ensures that the user ID is attached to the current session so that they do not have to reauthenticate for every request. For example: @@ -141,7 +187,7 @@ It is important to understand that this method is intended to be used for a user ### Signing out users -The ability to sign out users is provided by the [`marten-auth`](https://github.com/martenframework/marten-auth) shard through the use of the `MartenAuth#sign_out` method: this method takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html)) as argument, removes the authenticated user ID from the current request, and flushes the associated session data. +The ability to sign out users is provided by the [`marten-auth`](https://github.com/martenframework/marten-auth) shard through the use of the `MartenAuth#sign_out` method: this method takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html)) as argument, removes the authenticated user ID from the current request, and flushes the associated session data. For example: @@ -174,7 +220,7 @@ As mentioned previously, you should not attempt to manipulate the `password` fie ### Limiting access to signed-in users -Limiting access to signed-in users can easily be achieved by leveraging the `#user?` method that is available from [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html) objects. Using this method, you can easily implement [`#before_dispatch`](../handlers-and-http/introduction.md#before_dispatch) handler callbacks in order to redirect anonymous users to a sign-in page or to an error page. +Limiting access to signed-in users can easily be achieved by leveraging the `#user?` method that is available from [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html) objects. Using this method, you can easily implement [`#before_dispatch`](../handlers-and-http/callbacks.md#before_dispatch) handler callbacks in order to redirect anonymous users to a sign-in page or to an error page. For example: diff --git a/docs/versioned_docs/version-0.2/authentication/reference/generated-files.md b/docs/versioned_docs/version-0.5/authentication/reference/generated-files.md similarity index 73% rename from docs/versioned_docs/version-0.2/authentication/reference/generated-files.md rename to docs/versioned_docs/version-0.5/authentication/reference/generated-files.md index 288e705fc..284a800db 100644 --- a/docs/versioned_docs/version-0.2/authentication/reference/generated-files.md +++ b/docs/versioned_docs/version-0.5/authentication/reference/generated-files.md @@ -3,11 +3,11 @@ title: Generated files description: Generated files reference. --- -This page provides a reference of the files that are generated for the `auth` application when running the [`new`](../../development/reference/management-commands.md#new) management command with the `--with-auth` option. +This page provides a reference of the files that are generated for the `auth` application when running the [`new`](../../development/reference/management-commands.md#new) management command with the `--with-auth` option or when using the [`auth`](../../development/reference/generators.md#auth) generator. ## Application -The `auth` application is generated under the `src/auth` folder. In addition to the abstractions mentioned below, this folder defines the following top-level files: +The `auth` application is generated under the `src` or `src/apps` folder. In addition to the abstractions mentioned below, this folder defines the following top-level files: * `app.cr` - The entrypoint of the `auth` application, where all the other abstractions are required * `cli.cr` - The CLI entrypoint of the `auth` application, where CLI-related abstractions (like migrations) are required @@ -21,8 +21,9 @@ The `auth` application is generated under the `src/auth` folder. In addition to * `handlers/concerns/require_anonymous_user.cr` - A concern that ensures that a handler can only be accessed by anonymous users * `handlers/concerns/require_signed_in_user.cr` - A concern that ensures that a handler can only be accessed by signed-in users -* `handlers/password_reset_confirm_handler.cr` - A hander that handles resetting a user's password as part of the password reset flow -* `handlers/password_reset_initiate_handler.cr` - A hander that initiates the password reset flow for a given user +* `handlers/password_reset_confirm_handler.cr` - A handler that handles resetting a user's password as part of the password reset flow +* `handlers/password_reset_initiate_handler.cr` - A handler that initiates the password reset flow for a given user +* `handlers/password_update_handler.cr` - A handler that allows to update the user's password * `handlers/profile_handler.cr` - A handler that displays the currently signed-in user profile * `handlers/sign_in_handler.cr` - A handler that allows users to sign in * `handlers/sign_out_handler.cr` - A handler that allows users to sign out @@ -40,6 +41,7 @@ The `auth` application is generated under the `src/auth` folder. In addition to * `schemas/password_reset_confirm_schema.cr` - A schema that allows a user to reset their password * `schemas/password_reset_initiate_schema.cr` - A schema that allows a user to initiate the password reset flow +* `schemas/password_update_schema.cr` - A schema that allows a user to update their password * `schemas/sign_in_schema.cr` - A schema used to sign in users * `schemas/sign_up_schema.cr` - A schema used to sign up users @@ -48,10 +50,11 @@ The `auth` application is generated under the `src/auth` folder. In addition to * `templates/auth/emails/password_reset.html` - The template of the password reset email * `templates/auth/password_reset_confirm.html` - The template used to let users reset their passwords * `templates/auth/password_reset_initiate.html` - The template used to let users initiate the password reset flow +* `templates/auth/password_update.html` - The template used to let users update their password * `templates/auth/profile.html` - The template of the user profile * `templates/auth/sign_in.html` - The sign in page template * `templates/auth/sign_up.html` - The sign up page template ## Specs -All the previously mentioned abstractions have associated specs that are defined under the `spec/auth` folder. +All the previously mentioned abstractions have associated specs that are defined under the `spec/apps/auth` folder. diff --git a/docs/versioned_docs/version-0.5/caching.mdx b/docs/versioned_docs/version-0.5/caching.mdx new file mode 100644 index 000000000..d0e37df10 --- /dev/null +++ b/docs/versioned_docs/version-0.5/caching.mdx @@ -0,0 +1,31 @@ +--- +title: Caching +--- + +import DocCard from '@theme/DocCard'; + +Marten provides native support for caching, enabling you to store the outcome of resource-intensive operations and bypass performing them for each request. This encompasses basic caching abilities and advanced features like template fragment caching. + +## Guides + +
+
+ +
+
+ +## How-to's + +
+
+ +
+
+ +## Reference + +
+
+ +
+
diff --git a/docs/versioned_docs/version-0.5/caching/how-to/create-custom-cache-stores.md b/docs/versioned_docs/version-0.5/caching/how-to/create-custom-cache-stores.md new file mode 100644 index 000000000..55e50908c --- /dev/null +++ b/docs/versioned_docs/version-0.5/caching/how-to/create-custom-cache-stores.md @@ -0,0 +1,147 @@ +--- +title: Create cache stores +description: How to create custom cache stores. +--- + +Marten lets you easily create custom [cache stores](../introduction.md#configuration-and-cache-stores) that you can then use as part of your application when it comes to perform caching operations. + +## Basic store definition + +Defining a cache store is as simple as creating a class that inherits from the [`Marten::Caching::Store::Base`](pathname:///api/0.5/Marten/Cache/Store/Base.html) abstract class and that implements the following methods: + +* [`#clear`](pathname:///api/0.5/Marten/Cache/Store/Base.html#clear-instance-method) - called when clearing the cache +* [`#decrement`](pathname:///api/0.5/Marten/Cache/Store/Base.html#decrement(key%3AString%2Camount%3AInt32%3D1%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil)%3AInt-instance-method) - called when decrementing an integer value in the cache +* [`#delete_entry`](pathname:///http://localhost:3000/docs/api/dev/Marten/Cache/Store/Base.html#delete_entry%28key%3AString%29%3ABool-instance-method) - called when deleting an entry from the cache +* [`#increment`](pathname:///api/0.5/Marten/Cache/Store/Base.html#increment(key%3AString%2Camount%3AInt32%3D1%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil)%3AInt-instance-method) - called when incrementing an integer value in the cache +* [`#read_entry`](pathname:///api/0.5/Marten/Cache/Store/Base.html#read_entry(key%3AString)%3AString|Nil-instance-method) - called when reading an entry in the cache +* [`#write_entry`](pathname:///api/0.5/Marten/Cache/Store/Base.html#write_entry(key%3AString%2Cvalue%3AString%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil)-instance-method) - called when writing an entry to the cache + +For example, the following snippet implements an in-memory store that persists cache entries in a hash: + +```crystal +class MemoryStore < Marten::Cache::Store::Base + @data = {} of String => String + + def initialize( + @namespace : String? = nil, + @expires_in : Time::Span? = nil, + @version : Int32? = nil, + @compress = false, + @compress_threshold = DEFAULT_COMPRESS_THRESHOLD + ) + super + end + + def clear : Nil + @data.clear + end + + def decrement( + key : String, + amount : Int32 = 1, + expires_at : Time? = nil, + expires_in : Time::Span? = nil, + version : Int32? = nil, + race_condition_ttl : Time::Span? = nil, + compress : Bool? = nil, + compress_threshold : Int32? = nil + ) : Int + apply_increment( + key, + amount: -amount, + expires_at: expires_at, + expires_in: expires_in, + version: version, + race_condition_ttl: race_condition_ttl, + compress: compress, + compress_threshold: compress_threshold + ) + end + + def increment( + key : String, + amount : Int32 = 1, + expires_at : Time? = nil, + expires_in : Time::Span? = nil, + version : Int32? = nil, + race_condition_ttl : Time::Span? = nil, + compress : Bool? = nil, + compress_threshold : Int32? = nil + ) : Int + apply_increment( + key, + amount: amount, + expires_at: expires_at, + expires_in: expires_in, + version: version, + race_condition_ttl: race_condition_ttl, + compress: compress, + compress_threshold: compress_threshold + ) + end + + private getter data + + private def apply_increment( + key : String, + amount : Int32 = 1, + expires_at : Time? = nil, + expires_in : Time::Span? = nil, + version : Int32? = nil, + race_condition_ttl : Time::Span? = nil, + compress : Bool? = nil, + compress_threshold : Int32? = nil + ) + normalized_key = normalize_key(key.to_s) + entry = deserialize_entry(read_entry(normalized_key)) + + if entry.nil? || entry.expired? || entry.mismatched?(version || self.version) + write( + key: key, + value: amount.to_s, + expires_at: expires_at, + expires_in: expires_in, + version: version, + race_condition_ttl: race_condition_ttl, + compress: compress, + compress_threshold: compress_threshold + ) + amount + else + new_amount = entry.value.to_i + amount + entry = Entry.new(new_amount.to_s, expires_at: entry.expires_at, version: entry.version) + write_entry(normalized_key, serialize_entry(entry)) + new_amount + end + end + + private def delete_entry(key : String) : Bool + deleted_entry = @data.delete(key) + !!deleted_entry + end + + private def read_entry(key : String) : String? + data[key]? + end + + private def write_entry( + key : String, + value : String, + expires_in : Time::Span? = nil, + race_condition_ttl : Time::Span? = nil + ) + data[key] = value + true + end +end +``` + +## Enabling the use of custom cache stores + +Custom cache store can be used by assigning an instance of the corresponding class to the [`cache_store`](../../development/reference/settings.md#cache_store) setting. + +For example: + +```crystal +config.cache_store = MemoryStore.new +``` diff --git a/docs/versioned_docs/version-0.5/caching/introduction.md b/docs/versioned_docs/version-0.5/caching/introduction.md new file mode 100644 index 000000000..485c9a281 --- /dev/null +++ b/docs/versioned_docs/version-0.5/caching/introduction.md @@ -0,0 +1,153 @@ +--- +title: Introduction to caching +description: Learn how to leverage caching in a Marten project. +sidebar_label: Introduction +--- + +Marten provides a set of features allowing you to leverage caching as part of your application. By using caching, you can save the result of expensive operations so that you don't have to perform them for every request. + +## Configuration and cache stores + +In order to be able to leverage caching in your application, you need to configure a "cache store". A cache store allows interacting with the underlying cache system and performing basic operations such as fetching cached entries, writing new entries, etc. Depending on the chosen cache store, these operations could be performed in-memory or by leveraging external caching systems such as [Redis](https://redis.io) or [Memcached](https://memcached.org). + +The global cache store used by Marten can be configured by leveraging the [`cache_store`](../development/reference/settings.md#cache_store) setting. All the available cache stores are listed in the [cache store reference](./reference/stores.md). + +For example, the following configuration configures an in-memory cache as the global cache: + +```crystal +Marten.configure do |config| + config.cache_store = Marten::Cache::Store::Memory.new(expires_in: 24.hours) +end +``` + +:::info +By default, Marten uses an in-memory cache (instance of [`Marten::Cache::Store::Memory`](pathname:///api/0.5/Marten/Cache/Store/Memory.html)). Note that this simple in-memory cache does not allow to perform cross-process caching since each process running your app will have its own private cache instance. In situations where you have multiple separate processes running your application, it's preferable to use a proper caching system such as [Redis](https://redis.io) or [Memcached](https://memcached.org), which can be done by leveraging respectively the [`marten-redis-cache`](https://github.com/martenframework/marten-redis-cache) or [`marten-memcached-cache`](https://github.com/martenframework/marten-memcached-cache) shards. + +In testing environments, you could configure your project so that it uses an instance of [`Marten::Cache::Store::Null`](pathname:///api/0.5/Marten/Cache/Store/Null.html) as the global cache. This approach can be helpful when caching is not necessary, but you still want to ensure that your code is passing through the caching interface. +::: + +## Low-level caching + +### Basic usage + +Low-level caching allows you to interact directly with the global cache store and perform caching operations. To do that, you can access the global cache store by calling the [`Marten#cache`](pathname:///api/0.5/Marten.html#cache%3ACache%3A%3AStore%3A%3ABase-class-method) method. + +The main way to put new values in cache is to leverage the [`#fetch`](pathname:///api/0.5/Marten/Cache/Store/Base.html#fetch(key%3AString|Symbol%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Cforce%3Dfalse%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil%2C%26)%3AString|Nil-instance-method) method, which is provided on all cache stores. This method allows fetching data from the cache by using a specific key: if an entry exists for this key in cache, then the data is returned. Otherwise the return value of the block (that _must_ be specified when calling [`#fetch`](pathname:///api/0.5/Marten/Cache/Store/Base.html#fetch(key%3AString|Symbol%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Cforce%3Dfalse%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil%2C%26)%3AString|Nil-instance-method)) is written to the cache and returned. This method supports a few additional arguments that allow to further customize how the entry is written to the cache (eg. the expiry time associated with the entry). + +For example: + +```crystal +Marten.cache.fetch("mykey", expires_in: 4.hours) do + "myvalue" +end +``` + +### Reading from and writing to the cache + +It is worth mentioning that you can also explicitly read from the cache and write to the cache by leveraging the [`#read`](pathname:///api/0.5/Marten/Cache/Store/Base.html#read(key%3AString|Symbol%2Cversion%3AInt32|Nil%3Dnil)%3AString|Nil-instance-method) and [`#write`](pathname:///api/0.5/Marten/Cache/Store/Base.html#write(key%3AString|Symbol%2Cvalue%3AString%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil)-instance-method) methods respectively. Verifying that a key exists can be done using the [`#exists?`](pathname:///api/0.5/Marten/Cache/Store/Base.html#exists%3F(key%3AString|Symbol%2Cversion%3AInt32|Nil%3Dnil)%3ABool-instance-method) method. + +For example: + +```crystal +# No entry in the cache yet. +Marten.cache.read("foo") # => nil +Marten.cache.exists?("foo") # => false + +# Let's add the entry to the cache. +Marten.cache.write("foo", "bar", expires_in: 10.minutes) # => true + +# Let's read from the cache. +Marten.cache.read("foo") # => "bar" +Marten.cache.exists?("foo") # => true +``` + +### Deleting an entry from the cache + +Deleting an entry from the cache is made possible through the use of the [`#delete`](pathname:///api/0.5/Marten/Cache/Store/Base.html#delete(key%3AString|Symbol)%3ABool-instance-method) method. This method takes the key of the entry to delete as argument and returns a boolean indicating whether an entry was actually deleted. + +For example: + +```crystal +# No entry in the cache yet. +Marten.cache.delete("foo") # => false + +# Let's add an entry to the cache and then delete it. +Marten.cache.write("foo", "bar", expires_in: 10.minutes) # => true +Marten.cache.delete("foo") # => true +``` + +### Incrementing and decrementing values + +If you need to persist integer values that are intended to be incremented or decremented, then you can leverage the [`#increment`](pathname:///api/0.5/Marten/Cache/Store/Base.html#increment(key%3AString%2Camount%3AInt32%3D1%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil)%3AInt-instance-method) and [`#decrement`](pathname:///api/0.5/Marten/Cache/Store/Base.html#decrement(key%3AString%2Camount%3AInt32%3D1%2Cexpires_at%3ATime|Nil%3Dnil%2Cexpires_in%3ATime%3A%3ASpan|Nil%3Dnil%2Cversion%3AInt32|Nil%3Dnil%2Crace_condition_ttl%3ATime%3A%3ASpan|Nil%3Dnil%2Ccompress%3ABool|Nil%3Dnil%2Ccompress_threshold%3AInt32|Nil%3Dnil)%3AInt-instance-method) methods. The advantage of doing so is that the increment/decrement operation will be performed in an atomic fashion depending on the cache store you are using (eg. this is the case for the stores provided by the [`marten-memcached-cache`](https://github.com/martenframework/marten-memcached-cache) and [`marten-redis-cache`](https://github.com/martenframework/marten-redis-cache) shards). + +For example: + +```crystal +Marten.cache.increment("mycounter") # => 1 +Marten.cache.increment("mycounter", amount: 2) # => 3 +Marten.cache.decrement("mycounter") # => 2 +``` + +### Clearing the cache + +It's possible to fully clear the content of the cache by leveraging the [`#clear`](pathname:///api/0.5/Marten/Cache/Store/Base.html#clear-instance-method). + +For example: + +```crystal +# Let's add an entry to the cache and then let's clear the cache. +Marten.cache.write("foo", "bar", expires_in: 10.minutes) +Marten.cache.clear +``` + +:::caution +You should be extra careful when using this method because it will fully remove all the entries stored in the cache. Depending on the store implementation, only _namespaced_ entries may be removed (this is the case for the [Redis cache store](https://github.com/martenframework/marten-redis-cache) for example). +::: + +## Template fragment caching + +You can leverage template fragment caching when you want to cache some parts of your [templates](../templates.mdx). This capability is enabled by the use of the [`cache`](../templates/reference/tags.md#cache) template tag. + +This template tag allows caching the content of a template fragment (enclosed within the `{% cache %}...{% endcache %}` tags) for a specific duration. This caching operation is done by leveraging the configured [global cache store](#configuration-and-cache-stores). The tag itself takes at least two arguments: the name to give to the cache fragment and a cache timeout - expressed in seconds. + +For example, the following snippet caches the content enclosed within the `{% cache %}...{% endcache %}` tags for a duration of 3600 seconds and associates it to the "articles" fragment name: + +```html +{% cache "articles" 3600 %} + +{% endcache %} +``` + +It's worth noting that the [`cache`](../templates/reference/tags.md#cache) template tag allows for the inclusion of additional arguments. These arguments, referred to as "vary on" values, play a crucial role in generating the cache key of the template fragment. Essentially, the cache is invalidated if the value of any of these arguments changes. This feature comes in handy when you need to ensure that the template fragment is cached based on other dynamic values that may impact the generation of the cached content itself. + +For instance, suppose the cached content is dependent on the current locale. In that case, you'd want to make sure that the current locale value is taken into account while caching the template fragment. The ability to pass additional arguments as "vary on" values enables you to achieve precisely that. + +For example: + +```html +{% cache "articles" 3600 current_locale user.id %} + +{% endcache %} +``` + +:::tip +The "key" used for the template fragment cache entry can be a template variable. The same goes for the cache timeout. For example: + +```html +{% cache fragment_name fragment_expiry %} + +{% endcache %} +``` +::: diff --git a/docs/versioned_docs/version-0.5/caching/reference/stores.md b/docs/versioned_docs/version-0.5/caching/reference/stores.md new file mode 100644 index 000000000..2bba8c28a --- /dev/null +++ b/docs/versioned_docs/version-0.5/caching/reference/stores.md @@ -0,0 +1,46 @@ +--- +title: Caching stores +description: Caching stores reference. +sidebar_label: Stores +--- + +## Built-in stores + +### In-memory store + +This is the default store used as part of the [`cache_store`](../../development/reference/settings.md#cache_store) setting. + +This cache store is implemented as part of the [`Marten::Cache::Store::Memory`](pathname:///api/0.5/Marten/Cache/Store/Memory.html) class. This cache stores all data in memory within the same process, making it a fast and reliable option for caching in single process environments. However, it's worth noting that if you're running multiple instances of your application, the cache data will not be shared between them. + +For example: + +```crystal +Marten.configure do |config| + config.cache_store = Marten::Cache::Store::Memory.new(expires_in: 24.hours) +end +``` + +### Null store + +A cache store implementation doesn't store any data. + +This cache store is implemented as part of the [`Marten::Cache::Store::Null`](pathname:///api/0.5/Marten/Cache/Store/Null.html) class. This cache store does not store any data, but provides a way to go through the caching interface. This can be useful in development and testing environments when caching is not desired. + +For example: + +```crystal +Marten.configure do |config| + config.cache_store = Marten::Cache::Store::Null.new(expires_in: 24.hours) +end +``` + +## Other stores + +Additional cache stores shards are also maintained under the umbrella of the Marten project or by the community itself and can be used as part of your application depending on your specific caching requirements: + +* [`marten-memcached-cache`](https://github.com/martenframework/marten-memcached-cache) provides a [Memcached](https://memcached.org) cache store +* [`marten-redis-cache`](https://github.com/martenframework/marten-redis-cache) provides a [Redis](https://redis.io) cache store + +:::info +Feel free to contribute to this page and add links to your shards if you've created cache stores that are not listed here! +::: diff --git a/docs/versioned_docs/version-0.2/deployment.mdx b/docs/versioned_docs/version-0.5/deployment.mdx similarity index 64% rename from docs/versioned_docs/version-0.2/deployment.mdx rename to docs/versioned_docs/version-0.5/deployment.mdx index 1e884b97d..f6ecb29b6 100644 --- a/docs/versioned_docs/version-0.2/deployment.mdx +++ b/docs/versioned_docs/version-0.5/deployment.mdx @@ -20,4 +20,10 @@ The section explains the steps involved when it comes to deploying a Marten proj
+
+ +
+
+ +
diff --git a/docs/versioned_docs/version-0.2/deployment/how-to/deploy-to-an-ubuntu-server.md b/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-an-ubuntu-server.md similarity index 100% rename from docs/versioned_docs/version-0.2/deployment/how-to/deploy-to-an-ubuntu-server.md rename to docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-an-ubuntu-server.md diff --git a/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-fly-io.md b/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-fly-io.md new file mode 100644 index 000000000..7cc5e3373 --- /dev/null +++ b/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-fly-io.md @@ -0,0 +1,237 @@ +--- +title: Deploy to Fly.io +description: Learn how to deploy a Marten project to Fly.io. +--- + +This guide covers how to deploy a Marten project to [Fly.io](https://fly.io). + +## Prerequisites + +To complete the steps in this guide, you will need: + +* An active account on [Fly.io](https://fly.io). +* The Fly.io CLI [installed](https://fly.io/docs/hands-on/install-flyctl/) and correctly [configured](https://fly.io/docs/getting-started/log-in-to-fly/). +* A functional Marten project. + +## Make your Marten project Fly.io-ready + +Before creating the Fly.io application, it is important to ensure that your project is properly configured for deployment to Fly.io. This section outlines some steps to ensure that your project can be deployed to Fly.io without issues. + +### Create a `Dockerfile` + +We will be deploying our Marten project to Fly.io by leveraging a [Dockerfile strategy](https://fly.io/docs/languages-and-frameworks/dockerfile/). A `Dockerfile` is a text file that contains a set of instructions for building a [Docker](https://www.docker.com/) image. It typically includes a base image, commands to install dependencies, and steps to configure the environment and copy files into the image. + +Your `Dockerfile` should be placed at the root of your project folder and should contain the following content at least: + +```Dockerfile title="Dockerfile" +FROM crystallang/crystal:latest +WORKDIR /app +COPY . . + +ENV MARTEN_ENV=production + +RUN apt-get update +RUN apt-get install -y curl cmake build-essential + +RUN shards install +RUN bin/marten collectassets --no-input +RUN crystal build manage.cr -o bin/manage +RUN crystal build src/server.cr -o bin/server --release + +CMD ["/app/bin/server"] +``` + +As you can see, this Dockerfile builds a Docker image based on the latest version of the Crystal programming language image. It also installs your project's Crystal dependencies, runs the [`collectassets`](../../development/reference/management-commands.md) management command, and compiles your server's binary. + +It should be noted that this Dockerfile could perform additional operations if needed. For example, some projects may require Node.js in order to install additional dependencies and build your project's assets. This could be achieved with the following additions: + +```Dockerfile title="Dockerfile" +FROM crystallang/crystal:latest +WORKDIR /app +COPY . . + +ENV MARTEN_ENV=production + +RUN apt-get update +RUN apt-get install -y curl cmake build-essential +// highlight-next-line +RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +// highlight-next-line +RUN apt-get install -y nodejs +// highlight-next-line + +// highlight-next-line +RUN npm install +// highlight-next-line +RUN npm run build + +RUN shards install +RUN bin/marten collectassets --no-input +RUN crystal build manage.cr -o bin/manage +RUN crystal build src/server.cr -o bin/server --release + +CMD ["/app/bin/server"] +``` + +### Configure your production server's host and port + +You should ensure that your production server can be accessed from other containers, and on a specific port. To do so, it's important to set the [`host`](../../development/reference/settings.md#host) setting to `0.0.0.0` and the [`port`](../../development/reference/settings.md#port) setting to a specific value such as `8000` (which is the port we'll be using throughout this guide). + +This can be achieved by updating your `config/settings/production.cr` production settings file as follows: + +```crystal title="config/settings/production.cr" +Marten.configure :production do |config| + config.host = "0.0.0.0" + config.port = 8000 + + # Other settings... +end +``` + +### Configure key settings from environment variables + +When deploying to Fly.io, you will have to set a few environment variables (later in this guide) that will be used to populate key settings. This should be the case for the [`secret_key`](../../development/reference/settings.md#secret_key) and [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) settings at least. + +As such, it is important to ensure that your project populates these settings by reading their values in corresponding environment variables. This can be achieved by updating your `config/settings/production.cr` production settings file as follows: + +```crystal title="config/settings/production.cr" +Marten.configure :production do |config| + config.secret_key = ENV.fetch("MARTEN_SECRET_KEY", "") + config.allowed_hosts = ENV.fetch("MARTEN_ALLOWED_HOSTS", "").split(",") + + # Other settings... +end +``` + +It should be noted that if your application requires a database, you should also make sure to parse the `DATABASE_URL` environment variable and to configure your [database settings](../../development/reference/settings.md#database-settings) from the parsed database URL properties. The `DATABASE_URL` variable contains a URL-encoded string that specifies the connection details of your database, such as the database type, hostname, port, username, password, and database name. + +This can be accomplished as follows for a PostgreSQL database: + +```crystal title="config/settings/production.cr" +Marten.configure :production do |config| + if ENV.has_key?("DATABASE_URL") + # Note: DATABASE_URL isn't available at build time... + config.database do |db| + database_uri = URI.parse(ENV.fetch("DATABASE_URL")) + + db.backend = :postgresql + db.host = database_uri.host + db.port = database_uri.port + db.user = database_uri.user + db.password = database_uri.password + db.name = database_uri.path[1..] + + # Fly.io's Postgres works over an internal & encrypted network which does not support SSL. + # Hence, SSL must be disabled. + db.options = {"sslmode" => "disable"} + end + end + + # Other settings... +end +``` + +### Optional: set up the asset serving middleware + +In order to easily serve your application's assets in Fly.io, you can make use of the [`Marten::Middleware::AssetServing`](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware. Indeed, it won't be possible to configure a web server such as [Nginx](https://nginx.org) to serve your assets directly on Fly.io if you intend to use a "local file system" asset store (such as [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html)). + +To palliate this, you can make use of the [`Marten::Middleware::AssetServing`](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware. Obviously, this is not necessary if you intend to leverage a cloud storage provider (like Amazon's S3 or GCS) to store and serve your collected assets (in this case, you can simply skip this section). + +In order to use this middleware, you can "insert" the corresponding class at the beginning of the [`middleware`](../../development/reference/settings.md#middleware) setting when defining production settings. For example: + +```crystal +Marten.configure :production do |config| + config.middleware.unshift(Marten::Middleware::AssetServing) + + # Other settings... +end +``` + +The middleware will serve the collected assets available under the assets root ([`assets.root`](../../development/reference/settings.md#root) setting). It is also important to note that the [`assets.url`](../../development/reference/settings.md#url) setting must align with the Marten application domain or correspond to a relative URL path (e.g., `/assets/`) for this middleware to work correctly. + +## Create the Fly.io app + +To begin, the initial action required is to generate your Fly.io application itself. This can be achieved by executing the `fly launch` command as follows: + +```bash +fly launch --no-deploy --no-cache --internal-port 8000 --name --env MARTEN_ALLOWED_HOSTS=.fly.dev +``` + +The above command creates a Fly.io application whose internal port is set to `8000` while also ensuring that the `MARTEN_ALLOWED_HOSTS` environment variable is set to your future app domain. The command will ask you to choose a specific [region](https://fly.io/docs/reference/regions/) for your application and will create a `fly.toml` file whose content should look like this: + +```toml title="fly.toml" +app = "" +primary_region = "" + +[env] + MARTEN_ALLOWED_HOSTS = ".fly.dev" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true +``` + +The `fly.toml` is a configuration file used by Fly.io to know how to deploy your application to the Fly.io platform. + +:::info +In this guide, the `` placeholder refers to the Fly.io application name that you have chosen for your project. You should replace `` with the actual name of your application in all the relevant commands and code snippets mentioned in this guide. +::: + +## Set up environment secrets + +It is recommended to define the `MARTEN_SECRET_KEY` environment variable order to populate the [`secret_key`](../../development/reference/settings.md#secret_key) setting, as mentioned in [Configure key settings from environment variables](#configure-key-settings-from-environment-variables). + +Fly.io gives the ability to define such sensitive setting values using [runtime secrets](https://fly.io/docs/reference/secrets/). In this light, we can create a `MARTEN_SECRET_KEY` secret by using the `fly secrets` command as follows: + +```bash +fly secrets set MARTEN_SECRET_KEY=$(openssl rand -hex 16) +``` + +## Set up a database + +You'll need to provision a Fly.io PostgreSQL database if your application makes use of models and migrations (otherwise you can skip this step!). + +In this light, you first need to create a PostgreSQL cluster with the following command: + +```bash +fly pg create --name -db +``` + +Then you will need to "attach" the PostgreSQL cluster you just created with your actual application. This can be achieved with the following command: + +```bash +fly postgres attach -db --app +``` + +Additionally, you will want to ensure that migrations are automatically applied every time your project is deployed. To do so, you can update the `fly.toml` file that was generated previously and add the following section to it: + +```toml title="fly.toml" +app = "" +primary_region = "" + +[env] + MARTEN_ALLOWED_HOSTS = ".fly.dev" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + +// highlight-next-line +[deploy] +// highlight-next-line + release_command = "bin/manage migrate" +``` + +## Deploy the application + +The final step is to upload your application's code to Fly.io. This can be done by using the following command: + +```bash +fly deploy +``` + +It is worth mentioning that a few things will happen when you push your application's code to Fly.io like in the above example. Indeed, Fly.io will build a Docker image of your application based on the `Dockerfile` you defined [previously](#create-a-dockerfile) and then push it to the Fly.io registry (a private Docker registry maintained by Fly.io). Once this is done, it will launch a Docker container by using the obtained Docker image, and then route incoming traffic to the running container. diff --git a/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-heroku.md b/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-heroku.md new file mode 100644 index 000000000..2c7508614 --- /dev/null +++ b/docs/versioned_docs/version-0.5/deployment/how-to/deploy-to-heroku.md @@ -0,0 +1,215 @@ +--- +title: Deploy to Heroku +description: Learn how to deploy a Marten project to Heroku. +--- + +This guide covers how to deploy a Marten project to [Heroku](https://heroku.com). + +## Prerequisites + +To complete the steps in this guide, you will need: + +* An active account on [Heroku](https://heroku.com). +* The [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed and correctly configured. +* A functional Marten project. + +## Make your Marten project Heroku-ready + +Before creating the Heroku application, it is important to ensure that your project is properly configured for deployment to Heroku. This section outlines some necessary steps to ensure that your project can be deployed to Heroku without issues. + +### Create a `Procfile` + +You should first ensure that your project defines a [`Procfile`](https://devcenter.heroku.com/articles/procfile), at the root of your project folder. A Procfile specifies the commands Heroku's dynos run, defining process types like web servers and background workers. + +Your Procfile should contain the following content at least: + +```procfile title="Procfile" +web: bin/server --port \$PORT +``` + +:::info +The `PORT` environment variable is automatically defined by Heroku. That's why we have to ensure that its value is forwarded to your server. +::: + +If your application requires the use of a database, you should also add a [release process](https://devcenter.heroku.com/articles/procfile#the-release-process-type) that runs the [`migrate`](../../development/reference/management-commands.md#migrate) management command to your Procfile: + +```procfile title="Procfile" +web: bin/server --port \$PORT +release: marten migrate +``` + +This will ensure that your database is properly migrated during each deployment. + +### Configure the root path + +During deployment on Heroku, your application is prepared and compiled in a temporary directory, which is distinct from the location where the server runs your application. Specifically, the root of your application will be available under the `/app` folder on the Heroku platform. It's important to keep this in mind when setting up your application for deployment to Heroku. + +Marten's [application mechanism](../../development/applications.md) relies heaviliy on paths when it comes to locate things like [templates](../../templates.mdx), [translations](../../i18n.mdx), or [assets](../../assets.mdx). Because the path where your application is compiled will differ from the path where it runs, we need to ensure that you explicitly configure Marten so that it can find your project structure. + +To address this, we need to define a specific "root path" for your project in production. The root path specifies the actual location of the project sources in your system. This can prove helpful in scenarios where the project was compiled in a specific location different from the final destination where the project sources (and the `lib` folder) are copied, which is the case with Heroku. + +In this light, we can set the [`root_path`](../../development/reference/settings.md#root_path) setting to `/app` as follows: + +```crystal title="config/settings/production.cr" +Marten.configure :production do |config| + config.root_path = "/app" + + # Other settings... +end +``` + +As highlighted in the above example, this should be done in your "production" settings file. + +### Configure key settings from environment variables + +When deploying to Heroku, you will have to set a few environment variables (later in this guide) that will be used to populate key settings. This should be the case for the [`secret_key`](../../development/reference/settings.md#secret_key) and [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) settings at least. + +As such, it is important to ensure that your project populates these settings by reading their values in corresponding environment variables. This can be achieved by updating your `config/settings/production.cr` production settings file as follows: + +```crystal title="config/settings/production.cr" +Marten.configure :production do |config| + config.secret_key = ENV.fetch("MARTEN_SECRET_KEY") + config.allowed_hosts = ENV.fetch("MARTEN_ALLOWED_HOSTS", "").split(",") + + # Other settings... +end +``` + +It should be noted that if your application requires a database, you should also make sure to parse the `DATABASE_URL` environment variable and to configure your [database settings](../../development/reference/settings.md#database-settings) from the parsed database URL properties. The `DATABASE_URL` variable contains a URL-encoded string that specifies the connection details of your database, such as the database type, hostname, port, username, password, and database name. + +This can be accomplished as follows for a PostgreSQL database: + +```crystal title="config/settings/production.cr" +Marten.configure :production do |config| + config.database do |db| + database_uri = URI.parse(ENV.fetch("DATABASE_URL")) + + db.backend = :postgresql + db.host = database_uri.host + db.port = database_uri.port + db.user = database_uri.user + db.password = database_uri.password + db.name = database_uri.path[1..] + end + + # Other settings... +end +``` + +### Optional: set up the asset serving middleware + +In order to easily serve your application's assets in Heroku, you can make use of the [`Marten::Middleware::AssetServing`](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware. Indeed, it won't be possible to configure a web server such as [Nginx](https://nginx.org) to serve your assets directly on Heroku if you intend to use a "local file system" asset store (such as [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html)). + +To palliate this, you can make use of the [`Marten::Middleware::AssetServing`](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware. Obviously, this is not necessary if you intend to leverage a cloud storage provider (like Amazon's S3 or GCS) to store and serve your collected assets (in this case, you can simply skip this section). + +In order to use this middleware, you can "insert" the corresponding class at the beginning of the [`middleware`](../../development/reference/settings.md#middleware) setting when defining production settings. For example: + +```crystal +Marten.configure :production do |config| + config.middleware.unshift(Marten::Middleware::AssetServing) + + # Other settings... +end +``` + +The middleware will serve the collected assets available under the assets root ([`assets.root`](../../development/reference/settings.md#root) setting). It is also important to note that the [`assets.url`](../../development/reference/settings.md#url) setting must align with the Marten application domain or correspond to a relative URL path (e.g., `/assets/`) for this middleware to work correctly. + +## Create the Heroku app + +To begin, the initial action required is to generate your Heroku application itself. This can be achieved by executing the `heroku create` command as follows: + +```bash +heroku create +``` + +:::info +In this guide, the `` placeholder refers to the Heroku application name that you have chosen for your project. You should replace `` with the actual name of your application in all the relevant commands and code snippets mentioned in this guide. +::: + +## Set up the required buildpacks + +Heroku leverages [buildpacks](https://devcenter.heroku.com/articles/buildpacks) in order to "compile" web applications (which can include installing dependencies, compiling actual binaries, etc). In the context of a Marten project, it is recommended to use two buildbacks: + +1. First, the [Node.js official buildback](https://github.com/heroku/heroku-buildpack-nodejs) in order to "build" your project's assets. +2. Second, the [Marten official buildback](https://github.com/martenframework/heroku-buildpack-marten) in order to (i) compile your server's binary, (ii) compile the Marten CLI, and (iii) [collect assets](../../assets/introduction.md). + +The sequence of buildpacks applied during the deployment process is critical: the Node.js buildpack must be the first one applied, to ensure that Heroku can initiate the necessary Node.js installations and configurations. This approach will guarantee that when the Marten buildpack is activated, the assets will have already been created and are ready to be "collected" through the [`collectassets`](../../development/reference/management-commands.md#collectassets) management command. + +You can ensure that these buildpacks are used by running the following commands: + +```bash +heroku buildpacks:add heroku/nodejs +heroku buildpacks:add https://github.com/martenframework/heroku-buildpack-marten +``` + +:::tip +It is important to mention that the use of the [Node.js official buildback](https://github.com/heroku/heroku-buildpack-nodejs) is completely optional: you should only use it if your project leverages Node.js to build some assets. +::: + +## Set up environment variables + +### `MARTEN_ENV` + +At least one environment variable needs to be configured in order to ensure that your Marten project operates in production mode in Heroku: the `MARTEN_ENV` variable. This variable determines the current environments (and the associated settings to apply). + +To set this environment variable, you can leverage the `heroku config:set` command as follows: + +```bash +heroku config:set MARTEN_ENV=production +``` + +### `MARTEN_SECRET_KEY` + +It is also recommended to define the `MARTEN_SECRET_KEY` environment variable in order to populate the [`secret_key`](../../development/reference/settings.md#secret_key) setting, as mentioned in [Configure key settings from environment variables](#configure-key-settings-from-environment-variables). + +To set this environment variable, you can leverage the `heroku config:set` command as follows: + +```bash +heroku config:set MARTEN_SECRET_KEY=$(openssl rand -hex 16) +``` + +### `MARTEN_ALLOWED_HOSTS` + +Finally, we want to ensure that the [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) setting contains the actual domain of your Heroku application, which is required as part of Marten's [HTTP Host Header Attacks Protection mechanism](../../security/introduction.md#http-host-header-attacks-protection). + +To set this environment variable, you can use the following command: + +```bash +heroku config:set MARTEN_ALLOWED_HOSTS=.herokuapp.com +``` + +## Set up a database + +You'll need to provision a Heroku PostgreSQL database if your application makes use of models and migrations. To do so, you can make use of the following command: + +```bash +heroku addons:create heroku-postgresql:mini +``` + +:::info +You should replace `mini` in the above command by your desired [Postgres plan](https://devcenter.heroku.com/articles/heroku-postgres-plans). +::: + +## Upload the application + +The final step is to upload your application's code to Heroku. This can be done by using the standard `git push` command to copy the local `main` branch to the `main` branch on Heroku: + +```bash +git push heroku main +``` + +It is worth mentioning that a few things will happen when you push your application's code to Heroku like in the above example. Indeed, Heroku will detect the type of your application and apply the buildpacks you [configured previously](#set-up-the-required-buildpacks) (ie. first the Node.js one and then the Marten one). As part of this step, your application's dependencies will be installed and your project will be compiled. If you defined a `release` process in your `Procfile` (like explained in [Create a Procfile](#create-a-procfile)), the specfied command will also be executed (for example in order to run your project's migrations). + +A few additional things should also be noted: + +* The compiled server binary will be placed under the `bin/server` path. +* Your project's `manage.cr` file will be compiled as well and will be available by simply calling the `marten` command. This means that you can run `marten ` if you need to call specific [management commands](../../development/management-commands.md). +* The Marten buildpack will automatically call the [`collectassets`](../../development/reference/management-commands.md#collectassets) management command in order to collect your project's [assets](../../assets/introduction.md) and copy them to your configured assets storage. You can set the `DISABLE_COLLECTASSETS` environment variable to `1` if you don't want this behavior. + +## Run management commands + +If you need to run additional [management commands](../../development/management-commands.md) in your provisioned application, you can use the `heroku run` command. For instance: + +```bash +heroku run marten listmigrations +``` diff --git a/docs/versioned_docs/version-0.2/deployment/introduction.md b/docs/versioned_docs/version-0.5/deployment/introduction.md similarity index 96% rename from docs/versioned_docs/version-0.2/deployment/introduction.md rename to docs/versioned_docs/version-0.5/deployment/introduction.md index 1e63d4eb6..ace8d5985 100644 --- a/docs/versioned_docs/version-0.2/deployment/introduction.md +++ b/docs/versioned_docs/version-0.5/deployment/introduction.md @@ -120,3 +120,12 @@ Marten.configure do |config| # [...] end ``` + +:::tip +It is possible to generate a new secret key using the tools available. +Marten provides a secret key generator which can be used to generate a random key which can then be stored in an environment variable. + +```bash +bin/manage gen secretkey +``` +::: diff --git a/docs/versioned_docs/version-0.2/development.mdx b/docs/versioned_docs/version-0.5/development.mdx similarity index 73% rename from docs/versioned_docs/version-0.2/development.mdx rename to docs/versioned_docs/version-0.5/development.mdx index e8135349c..dc566b2e8 100644 --- a/docs/versioned_docs/version-0.2/development.mdx +++ b/docs/versioned_docs/version-0.5/development.mdx @@ -18,6 +18,9 @@ Marten provides various tools and mechanisms that you can leverage in order to d
+
+ +
@@ -26,6 +29,9 @@ Marten provides various tools and mechanisms that you can leverage in order to d ## How-to's
+
+ +
@@ -40,4 +46,7 @@ Marten provides various tools and mechanisms that you can leverage in order to d
+
+ +
diff --git a/docs/versioned_docs/version-0.2/development/applications.md b/docs/versioned_docs/version-0.5/development/applications.md similarity index 59% rename from docs/versioned_docs/version-0.2/development/applications.md rename to docs/versioned_docs/version-0.5/development/applications.md index 51ee1ffdf..e88dca60e 100644 --- a/docs/versioned_docs/version-0.2/development/applications.md +++ b/docs/versioned_docs/version-0.5/development/applications.md @@ -4,11 +4,11 @@ description: Learn how to leverage applications to structure your projects. sidebar_label: Applications --- -Marten projects can be organized into logical and reusable components called "applications". These applications can contribute specific behaviors and abstractions to a project, including [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), and [templates](../templates.mdx). They can be packaged and reused across various projects as well. +Marten projects can be organized into logical and reusable components called "applications". These applications can contribute specific behaviors and abstractions to a project, including [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [schemas](../schemas/introduction.md), [emails](../emailing/introduction.md), and [templates](../templates.mdx). They can be packaged and reused across various projects as well. ## Overview -A Marten **application** is a set of abstractions (defined under a dedicated and unique folder) that provides some set of features. These abstractions can correspond to [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [templates](../templates.mdx), [schemas](../schemas.mdx), etc. +A Marten **application** is a set of abstractions (defined under a dedicated and unique folder) that provides some set of features. These abstractions can correspond to [models](../models-and-databases.mdx), [handlers](../handlers-and-http.mdx), [templates](../templates.mdx), [schemas](../schemas.mdx), [emails](../emailing/introduction.md), etc. Marten projects always use one or many applications. Indeed, each Marten project comes with a default [main application](#the-main-application) that corresponds to the standard `src` folder: models, migrations, or other classes defined in this folder are associated with the main application by default (unless they are part of another _explicitly defined_ application). As projects grow in size and scope, it is generally encouraged to start thinking in terms of applications and how to split models, handlers, or features across multiple apps depending on their intended responsibilities. @@ -16,9 +16,9 @@ Another benefit of applications is that they can be packaged and reused across m ## Using applications -The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installedapps) setting. +The use of applications must be manually enabled within projects: this is done through the use of the [`installed_apps`](./reference/settings.md#installed_apps) setting. -This setting corresponds to an array of installed app classes. Indeed, each Marten application must define a subclass of [`Marten::App`](pathname:///api/0.2/Marten/App.html) to specify a few things such as the application label (see [Creating applications](#creating-applications) for more information about this). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. +This setting corresponds to an array of installed app classes. Indeed, each Marten application must define a subclass of [`Marten::App`](pathname:///api/0.5/Marten/App.html) to specify a few things such as the application label (see [Creating applications](#creating-applications) for more information about this). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. For example: @@ -40,7 +40,7 @@ Adding an application class inside this array will have the following impact on ### The main application -The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installedapps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. +The "main" application is a default application that is always implicitly used by Marten projects (which means that it does not appear in the [`installed_apps`](./reference/settings.md#installed_apps) setting). This application is associated with the standard `src` folder: this means that models, migrations, assets, or templates defined in this folder will be associated with the main application by default. For example, models defined under a `src/models` folder would be associated with the main application. :::info The main application is associated with the `main` label. This means that models of the main application that do not define an explicit table name will have table names starting with `main_`. @@ -52,7 +52,7 @@ In the end, the main application provides a convenient way for starting projects ### Order of installed applications -You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installedapps) setting can actually matter. +You should note that the order in which installed applications are defined in the [`installed_apps`](./reference/settings.md#installed_apps) setting can actually matter. For example, a "foo" app might define a `test.html` template, and a similar template with the exact same name might be defined by a "bar" app. If the "foo" app appears before the "bar" app in the array of installed apps, then requesting and rendering the `test.html` template will actually involve the "foo" app's template only. This is because template loaders associated with app directories iterate over applications in the order in which they are defined in the installed apps array. @@ -60,16 +60,17 @@ This is why it is always important to _namespace_ abstractions, assets, template ## Creating applications -Creating applications can be done very easily through the use of the [`new`](./reference/management-commands.md#new) management command. For example: +Creating applications can be done very easily through the use of the [`app`](./reference/generators.md#app) generator. For example: ```bash -marten new app blog --dir=src/blog +marten gen app blog ``` -Running such a command will usually create the following directory structure: +Running such a command will add a new `blog` application to the current project with the following structure: ``` src/blog +├── emails ├── handlers ├── migrations ├── models @@ -83,21 +84,33 @@ These files and folders are described below: | Path | Description | | ----------- | ----------- | -| handlers/ | Empty directory where the request handlers of the application will be defined. | -| migrations/ | Empty directory that will store the migrations that will be generated for the models of the application. | -| models/ | Empty directory where the models of the application will be defined. | -| schemas/ | Empty directory where the schemas of the application will be defined. | -| templates/ | Empty directory where the templates of the application will be defined. | -| app.cr | Definition of the application configuration abstraction; this is also where application files requirements should be defined. | -| cli.cr | Requirements of CLI-related files, such as migrations for example. | +| `emails/` | Empty directory where the [emails](../emailing/introduction.md) of the application will be defined. | +| `handlers/` | Empty directory where the [request handlers](../handlers-and-http/introduction.md) of the application will be defined. | +| `migrations/` | Empty directory that will store the [migrations](../models-and-databases/migrations.md) that will be generated for the models of the application. | +| `models/` | Empty directory where the [models](../models-and-databases/introduction.md) of the application will be defined. | +| `schemas/` | Empty directory where the [schemas](../schemas/introduction.md) of the application will be defined. | +| `templates/` | Empty directory where the [templates](../templates/introduction.md) of the application will be defined. | +| `app.cr` | Definition of the application configuration abstraction; this is also where application-specific file requirements should be made. | +| `cli.cr` | Requirements of CLI-related files, such as migrations for example. | +| `routes.cr` | Module containing the [routes](../handlers-and-http/routing.md) of the application. | + +:::tip +The [`app`](./reference/generators.md#app) generator automatically ensures that: + +* The newly created application is added to the [`installed_apps`](./reference/settings.md#installed_apps) setting. +* Requirements for the application itself are added to the `src/project.cr` and `src/cli.cr` files. +* The application's routes are included in the main routes map (which lives in the `config/routes.cr` file). +::: -The most important file of an application is the `app.cr` one. This file usually includes all the app requirements and defines the application configuration class itself, which must be a subclass of the [`Marten::App`](pathname:///api/0.2/Marten/App.html) abstract class. This class allows mainly to define the "label" identifier of the application (through the use of the [`#label`](pathname:///api/0.2/Marten/Apps/Config.html#label(label%3AString|Symbol)-class-method) class method): this identifier must be unique across all the installed applications of a project and is used to generate things like model table names or migration classes. +The most important file of an application is the `app.cr` one. This file usually includes all the app requirements and defines the application configuration class itself, which must be a subclass of the [`Marten::App`](pathname:///api/0.5/Marten/App.html) abstract class. This class allows mainly to define the "label" identifier of the application (through the use of the [`#label`](pathname:///api/0.5/Marten/Apps/Config.html#label(label%3AString|Symbol)-class-method) class method): this identifier must be unique across all the installed applications of a project and is used to generate things like model table names or migration classes. Here is an example `app.cr` file content for a hypothetic "blog" app: ```crystal +require "./emails/**" require "./handlers/**" require "./models/**" +require "./routes" require "./schemas/**" module Blog @@ -117,3 +130,36 @@ Another very important file is the `cli.cr` one: this file is there to define al require "./cli/**" require "./migrations/**" ``` + +### Defining settings for applications + +Applications that you create as part of your projects or third-party libraries can have their own associated settings, configurable through the use of regular [settings files](./settings.md). + +In order to define settings for your applications, the simplest way is to create a `settings.cr` file containing a subclass of [`Marten::Conf::Settings`](pathname:///api/0.5/Marten/Conf/Settings.html) in your application's folder. This subclass must make use of the [`#namespace`](pathname:///api/0.5/Marten/Conf/Settings.html#namespace(ns)-macro) macro in order to define the setting "namespace" under which your application's settings will be accessible. + +For example: + +```crystal +module Blog + class Settings < Marten::Conf::Settings + namespace :blog + + @my_setting : String = "foo" + + getter my_setting + setter my_setting + end +end +``` + +With the above example, it will be possible to configure the `blog.my_setting` setting as follows in a project's settings file: + +```crystal +Marten.configure do |config| + config.blog.my_setting = "bar" +end +``` + +As you can see, the application's settings are configurable like any other built-in settings, but they are namespaced to the namespace value that was defined in the `Blog::Settings` class through the use of the [`#namespace`](pathname:///api/0.5/Marten/Conf/Settings.html#namespace(ns)-macro) macro. + +It's important to note that [`Marten::Conf::Settings`](pathname:///api/0.5/Marten/Conf/Settings.html) subclasses have the flexibility to define any necessary methods to facilitate user configuration for the considered application. While basic settings typically necessitate only getters and setters for configuration, more intricate scenarios may demand additional methods, the utilization of blocks, or other complexities. diff --git a/docs/versioned_docs/version-0.5/development/generators.md b/docs/versioned_docs/version-0.5/development/generators.md new file mode 100644 index 000000000..54c1b6457 --- /dev/null +++ b/docs/versioned_docs/version-0.5/development/generators.md @@ -0,0 +1,107 @@ +--- +title: Generators +description: Learn how to use generators in Marten. +--- + +Marten features a generator mechanism that simplifies the creation of various abstractions, files, and structures within an existing project. This feature facilitates the generation of key components such as [models](../models-and-databases/introduction.md), [schemas](../schemas/introduction.md), [emails](../emailing/introduction.md), or [applications](./applications.md). By leveraging generators, developers can improve their workflow and speed up the development of their Marten projects while following best practices. + +## Usage + +Generators can be invoked by leveraging the [`marten gen`](./reference/management-commands.md#gen) management command. This command is intended to be used as follows: + +```bash +marten gen [generator] [options] [arguments] +``` + +As you can see, the `marten gen` command must be used with a specific **generator** name, possibly followed by **options** and **arguments** (which may be required or not depending on the considered generator). All the built-in generators are listed in the [generators reference](./reference/generators.md). + +### Displaying help information + +You can display help information about a specific generator by using the `marten gen` command as follows: + +```bash +marten gen [generator] --help +``` + +### Listing generators + +It is possible to list all the available generators within a project by running the `marten gen` command as follows: + +```bash +marten gen +``` + +This should output something like this: + +``` +Usage: marten gen [options] [generator] + +Generate various structures, abstractions, and values within an existing project. + +Arguments: + generator Name of the generator to use + +Options: + --error-trace Show full error trace (if a compilation is involved) + --no-color Disable colored output + -h, --help Show this help + +Available generators are listed below. + +[marten] + + › app + › auth + › email + › handler + › model + › schema + › secretkey + +Run a generator followed by --help to see generator specific information, ex: +marten gen [generator] --help +``` + +## Examples + +### Generating a model + +Generating a model can be achieved with the [`model`](./reference/generators.md#model) generator: + +```bash +# Generate a model in the main app: +marten gen model User name:string email:string + +# Generate a model in the admin app: +marten gen model User name:string email:string --app admin + +# Generate a model with a many-to-one reference: +marten gen model Article label:string body:text author:many_to_one{User} + +# Generate a model with a parent class: +marten gen model Admin::User name:string email:string --parent User + +# Generate a model without timestamps: +marten gen model User name:string email:string --no-timestamps +``` + +### Generating an email + +Generating an email can be achieved with the [`email`](./reference/generators.md#email) generator: + +```bash +marten gen email TestEmail # Generate a new TestEmail email in the main application +marten gen email TestEmail --app blog # Generate a new TestEmail email in the blog application +``` + +### Generating an application + +Generating an application can be achieved with the [`app`](./reference/generators.md#app) generator: + +```bash +marten gen app blogging # Generate a new 'blogging' application +``` + +## Available generators + +Please head over to the [generators reference](./reference/generators.md) to see a list of all the available generators. diff --git a/docs/versioned_docs/version-0.5/development/how-to/configure-database-backends.md b/docs/versioned_docs/version-0.5/development/how-to/configure-database-backends.md new file mode 100644 index 000000000..8acc1e5c4 --- /dev/null +++ b/docs/versioned_docs/version-0.5/development/how-to/configure-database-backends.md @@ -0,0 +1,156 @@ +--- +title: Configure database backends +description: How to configure database backends. +--- + +This guide provides instructions on configuring new database backends or changing the existing database backend within your existing Marten projects. + +## Context + +Marten officially supports **MariaDB**, **MySQL**, **PostgreSQL**, and **SQLite3** databases. New Marten projects default to utilizing a SQLite3 database, a lightweight serverless database application that is typically pre-installed on most existing operating systems. This makes it an excellent choice for a development or testing database, but you may want to use a more powerful database such as MariaDB, MySQL, or PostgreSQL. In this light, this guide explains what steps should be taken in order to use your database backend of choice in a Marten project. + +## Prerequisites + +This guide presupposes that you already have a functional Marten project available. If you don't, you can easily create one using the following command: + +```bash +marten new project +``` + +Furthermore, it assumes that your preferred database is properly configured and ready for use. If this isn't the case, please consult the respective official documentation to install your chosen database: + +* [PostgreSQL Installation Guide](https://wiki.postgresql.org/wiki/Detailed_installation_guides) +* [MariaDB Installation Guide](https://mariadb.com/kb/en/getting-installing-and-upgrading-mariadb) +* [MySQL Installation Guide](https://dev.mysql.com/doc/refman/8.0/en/installing.html) +* [SQLite Installation Guide](https://www.tutorialspoint.com/sqlite/sqlite_installation.htm) + +## Installing the right database shard + +For each database, a dedicated Crystal shard is required. Depending on your chosen database, you must include one of the following entries in your project's `shard.yml` file: + +* [crystal-pg](https://github.com/will/crystal-pg) (required for PostgreSQL databases) +* [crystal-mysql](https://github.com/crystal-lang/crystal-mysql) (required for MariaDB or MySQL databases) +* [crystal-sqlite3](https://github.com/crystal-lang/crystal-sqlite3) (required for SQLite3 databases) + +This means that your `shard.yml` file should resemble one of the following examples: + +### MariaDB or MySQL + +```yaml +name: myproject +version: 0.1.0 + +dependencies: + marten: + github: martenframework/marten + // highlight-next-line + mysql: + // highlight-next-line + github: crystal-lang/crystal-mysql +``` + +### PostgreSQL + +```yaml +name: myproject +version: 0.1.0 + +dependencies: + marten: + github: martenframework/marten + // highlight-next-line + pg: + // highlight-next-line + github: will/crystal-pg +``` + +### SQLite3 + +```yaml +name: myproject +version: 0.1.0 + +dependencies: + marten: + github: martenframework/marten + // highlight-next-line + sqlite3: + // highlight-next-line + github: crystal-lang/crystal-sqlite3 +``` + +## Adding the right DB Crystal requirement + +After you've included the correct Crystal shard in your project's `shard.yml` file, the subsequent task is to add the corresponding requirement in the `src/project.cr` file. This file contains all the requirements of your project (including Marten itself) and is automatically generated by the [`new`](../reference/management-commands.md#new) management command. + +Please consult the examples below to determine which requirement you should include based on your selected database backend: + +### MariaDB or MySQL + +```crystal +# Third party requirements. +require "marten" +// highlight-next-line +require "mysql" + +# Project requirements. +# [...] + +# Configuration requirements. +# [...] +``` + +### PostgreSQL + +```crystal +# Third party requirements. +require "marten" +// highlight-next-line +require "pg" +``` + +### SQLite3 + +```crystal +# Third party requirements. +require "marten" +// highlight-next-line +require "sqlite3" +``` + +## Configuring your database + +The last step involves configuring database settings so that they target the database you intend to use with your Marten project. While a comprehensive list of configuration options is available in the [Database settings reference](../reference/settings.md#database-settings), the following sections offer example configurations tailored to each supported database backend. + +### MariaDB or MySQL + +```crystal +config.database do |db| + db.backend = :mysql + db.host = "localhost" + db.name = "my_db" + db.user = "my_user" + db.password = "insecure" +end +``` + +### PostgreSQL + +```crystal +config.database do |db| + db.backend = :postgresql + db.host = "localhost" + db.name = "my_db" + db.user = "my_user" + db.password = "insecure" +end +``` + +### SQLite3 + +```crystal +config.database do |db| + db.backend = :sqlite + db.name = "my_db.db" +end +``` diff --git a/docs/versioned_docs/version-0.2/development/how-to/create-custom-commands.md b/docs/versioned_docs/version-0.5/development/how-to/create-custom-commands.md similarity index 85% rename from docs/versioned_docs/version-0.2/development/how-to/create-custom-commands.md rename to docs/versioned_docs/version-0.5/development/how-to/create-custom-commands.md index c7d8c8651..c866c3e07 100644 --- a/docs/versioned_docs/version-0.2/development/how-to/create-custom-commands.md +++ b/docs/versioned_docs/version-0.5/development/how-to/create-custom-commands.md @@ -7,9 +7,9 @@ Marten lets you create custom management commands as part of your [applications] ## Basic management command definition -Custom management commands are defined as subclasses of the [`Marten::CLI::Command`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html) abstract class. Such subclasses should be defined in a `cli/` folder at the root of the application, and it should be ensured that they are required by your `cli.cr` file (see [Creating applications](../applications.md#creating-applications) for more details regarding the structure of an application). +Custom management commands are defined as subclasses of the [`Marten::CLI::Command`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html) abstract class. Such subclasses should be defined in a `cli/` folder at the root of the application, and it should be ensured that they are required by your `cli.cr` file (see [Creating applications](../applications.md#creating-applications) for more details regarding the structure of an application). -Management command classes must at least define a [`#run`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#run-instance-method) method, which will be called when the subcommand is executed: +Management command classes must at least define a [`#run`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#run-instance-method) method, which will be called when the subcommand is executed: ```crystal class MyCommand < Marten::CLI::Command @@ -21,7 +21,7 @@ class MyCommand < Marten::CLI::Command end ``` -As you can see in the previous example, the [`#help`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#help(help%3AString)-class-method) class method allows setting a "help text" that will be displayed when the help information of the command is requested. +As you can see in the previous example, the [`#help`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#help(help%3AString)-class-method) class method allows setting a "help text" that will be displayed when the help information of the command is requested. If the above command was part of an installed application, it could be executed by using the Marten CLI as follows: @@ -38,7 +38,7 @@ Marten management commands can accept options and arguments. These differ and ma By default options and arguments are always optional. That being said, they can be made mandatory in the command execution logic if needed. -Both options and arguments must be specified in the optional [`#setup`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#setup-instance-method) method: this method will be called to prepare the definition of the command, including its arguments and options. +Both options and arguments must be specified in the optional [`#setup`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#setup-instance-method) method: this method will be called to prepare the definition of the command, including its arguments and options. For example: @@ -60,7 +60,7 @@ class MyCommand < Marten::CLI::Command end ``` -In the above example, the [`#on_argument`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#on_argument(name%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method is used to define an `arg1` argument. This method requires an argument name, an associated help text, and a proc where the value of the argument will be forwarded at execution time (which allows you to assign it to an instance variable or process it if you wish to). Similarly, the [`#on_option`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#on_option(flag%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method is used to define an `example` option. In this case, the name of the option and its associated help text must be specified, and a proc can be defined to identify that the option was specified at execution time (which can be used to set a related boolean instance variable for example). +In the above example, the [`#on_argument`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#on_argument(name%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method is used to define an `arg1` argument. This method requires an argument name, an associated help text, and a proc where the value of the argument will be forwarded at execution time (which allows you to assign it to an instance variable or process it if you wish to). Similarly, the [`#on_option`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#on_option(flag%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method is used to define an `example` option. In this case, the name of the option and its associated help text must be specified, and a proc can be defined to identify that the option was specified at execution time (which can be used to set a related boolean instance variable for example). The above command would produce the following help information: @@ -81,7 +81,7 @@ Options: ### Configuring options -As mentioned previously, it is possible to make use of the [`#on_option`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#on_option(flag%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method to configure a specific command option (eg. `--option`). It expects a flag name and a description, and it yields a block to let the command properly assign the option value to the command object at execution time: +As mentioned previously, it is possible to make use of the [`#on_option`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#on_option(flag%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method to configure a specific command option (eg. `--option`). It expects a flag name and a description, and it yields a block to let the command properly assign the option value to the command object at execution time: ```crystal on_option("example", "An example option") { @example = true } @@ -97,7 +97,7 @@ on_option("e", "example", "An example option") { @example = true } ### Configuring options that accept arguments -It is possible to make use of the [`#on_option_with_arg`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#on_option_with_arg(flag%3AString|Symbol%2Carg%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method to configure a specific command option with an associated argument. This method will configure a command option (eg. `--option`) and an associated argument. It expects a flag name, an argument name, and a description. It yields a block to let the command properly assign the option to the command object at execution time: +It is possible to make use of the [`#on_option_with_arg`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#on_option_with_arg(flag%3AString|Symbol%2Carg%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method to configure a specific command option with an associated argument. This method will configure a command option (eg. `--option`) and an associated argument. It expects a flag name, an argument name, and a description. It yields a block to let the command properly assign the option to the command object at execution time: ```crystal on_option_with_arg(:option, :arg, "The name of the option") { @arg = arg } @@ -111,7 +111,7 @@ on_option_with_arg("o", "option", "arg", "The name of the option") { |arg| @arg ### Configuring arguments -As mentioned previously, it is possible to make use of the [`#on_argument`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#on_argument(name%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method in order to configure a specific command argument. This method expects an argument name and a description, and it yields a block to let the command properly assign the argument value to the command object at execution time: +As mentioned previously, it is possible to make use of the [`#on_argument`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#on_argument(name%3AString|Symbol%2Cdescription%3AString%2C%26block%3AString->)-instance-method) instance method in order to configure a specific command argument. This method expects an argument name and a description, and it yields a block to let the command properly assign the argument value to the command object at execution time: ```crystal on_argument(:arg, "The name of the argument") { |value| @arg_var = value } @@ -123,7 +123,7 @@ It should be noted that the order in which arguments are defined is important: t ## Outputting text contents -When writing management commands, you will likely need to write text contents to the output file descriptor. To do so, you can make use of the [`#print`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#print(msg%2Cending%3D"\n")-instance-method) instance method: +When writing management commands, you will likely need to write text contents to the output file descriptor. To do so, you can make use of the [`#print`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#print(msg%2Cending%3D"\n")-instance-method) instance method: ```crystal class HelloWorldCommand < Marten::CLI::Command @@ -135,7 +135,7 @@ class HelloWorldCommand < Marten::CLI::Command end ``` -It should be noted that you can also choose to "style" the content you specify to [`#print`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#print(msg%2Cending%3D"\n")-instance-method) by wrapping your string with a call to the [`#style`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#style(msg%2Cfore%3Dnil%2Cmode%3Dnil)-instance-method) method. For example: +It should be noted that you can also choose to "style" the content you specify to [`#print`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#print(msg%2Cending%3D"\n")-instance-method) by wrapping your string with a call to the [`#style`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#style(msg%2Cfore%3Dnil%2Cmode%3Dnil)-instance-method) method. For example: ```crystal class HelloWorldCommand < Marten::CLI::Command @@ -147,11 +147,11 @@ class HelloWorldCommand < Marten::CLI::Command end ``` -As you can see, the [`#style`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#style(msg%2Cfore%3Dnil%2Cmode%3Dnil)-instance-method) method can be used to apply `fore` and `mode` styles to a specific text value. The values you can use for the `fore` and `mode` arguments are the same as the ones that you can use with the [`Colorize`](https://crystal-lang.org/api/Colorize.html) module (which comes with the standard library). +As you can see, the [`#style`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#style(msg%2Cfore%3Dnil%2Cmode%3Dnil)-instance-method) method can be used to apply `fore` and `mode` styles to a specific text value. The values you can use for the `fore` and `mode` arguments are the same as the ones that you can use with the [`Colorize`](https://crystal-lang.org/api/Colorize.html) module (which comes with the standard library). ## Handling error cases -You will likely want to handle error situations when writing management commands. For example, to return error messages if a specified argument is not provided or if it is invalid. To do so you can make use of the [`#print_error`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#print_error(msg)-instance-method) helper method, which will print the passed string to the error file descriptor: +You will likely want to handle error situations when writing management commands. For example, to return error messages if a specified argument is not provided or if it is invalid. To do so you can make use of the [`#print_error`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#print_error(msg)-instance-method) helper method, which will print the passed string to the error file descriptor: ```crystal class HelloWorldCommand < Marten::CLI::Command @@ -173,11 +173,11 @@ class HelloWorldCommand < Marten::CLI::Command end ``` -Alternatively, you can make use of the [`#print_error_and_exit`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#print_error_and_exit(msg%2Cexit_code%3D1)-instance-method) method to print a message to the error file descriptor and to exit the execution of the command. +Alternatively, you can make use of the [`#print_error_and_exit`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#print_error_and_exit(msg%2Cexit_code%3D1)-instance-method) method to print a message to the error file descriptor and to exit the execution of the command. ## Customizing the subcommand name -By default, management command names are inferred by using their associated class names (eg. a `MyCommand` command class would translate to a `my_command` subcommand). That being said, it should be noted that you can define a custom subcommand name by leveraging the [`#command_name`](pathname:///api/0.2/Marten/CLI/Manage/Command/Base.html#command_name(name%3AString|Symbol)-class-method) class method: +By default, management command names are inferred by using their associated class names (eg. a `MyCommand` command class would translate to a `my_command` subcommand). That being said, it should be noted that you can define a custom subcommand name by leveraging the [`#command_name`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#command_name(name%3AString|Symbol)-class-method) class method: ```crystal class MyCommand < Marten::CLI::Command @@ -189,3 +189,17 @@ class MyCommand < Marten::CLI::Command end end ``` + +It is also worth mentioning that command aliases can be configured easily by using the [`#command_aliases`](pathname:///api/0.5/Marten/CLI/Manage/Command/Base.html#command_aliases(*aliases%3AString|Symbol)-class-method) helper method. For example: + +```crystal +class MyCommand < Marten::CLI::Command + command_name :test + command_aliases :t + help "Command that does something" + + def run + # Do something + end +end +``` diff --git a/docs/versioned_docs/version-0.2/development/management-commands.md b/docs/versioned_docs/version-0.5/development/management-commands.md similarity index 78% rename from docs/versioned_docs/version-0.2/development/management-commands.md rename to docs/versioned_docs/version-0.5/development/management-commands.md index 3dc71ba07..35657d0f4 100644 --- a/docs/versioned_docs/version-0.2/development/management-commands.md +++ b/docs/versioned_docs/version-0.5/development/management-commands.md @@ -40,20 +40,23 @@ marten help This should output something like this: ```bash -Usage: marten [command] [arguments] +Usage: marten [command] [options] [arguments] Available commands: [marten] - › collectassets - › genmigrations - › listmigrations - › migrate - › new - › resetmigrations - › routes - › serve - › version + + › clearsessions Clear all expired sessions. + › collectassets Collect all the assets and copy them in a unique storage. + › gen / g Generate various structures, abstractions, and values within an existing project. + › genmigrations Generate new database migrations. + › listmigrations List all database migrations. + › migrate Run database migrations. + › new Initialize a new Marten project or application repository. + › resetmigrations Reset an existing set of migrations into a single one. + › routes Display all the routes of the application. + › serve / s Start a development server that is automatically recompiled when source files change. + › version Show the Marten version. Run a command followed by --help to see command specific information, ex: marten [command] --help diff --git a/docs/versioned_docs/version-0.5/development/reference/generators.md b/docs/versioned_docs/version-0.5/development/reference/generators.md new file mode 100644 index 000000000..e47959df8 --- /dev/null +++ b/docs/versioned_docs/version-0.5/development/reference/generators.md @@ -0,0 +1,194 @@ +--- +title: Generators +description: Generators reference. +toc_max_heading_level: 2 +--- + +This page provides a reference for all the available generators and their options. + +## `app` + +**Usage:** `marten gen app [options] [label]` + +Add and configure a new [application](../applications.md) to the current project. + +:::info +This generator will attempt to add the generated application to the [`installed_apps`](./settings.md#installed_apps) setting and will also configure Crystal requirements for it (in the `src/project.cr` and `src/cli.cr` files). +::: + +### Arguments + +* `label` - Label of the application to generate + +### Examples + +```bash +marten gen app blogging # Generate a new 'blogging' application +``` + +## `auth` + +**Usage:** `marten gen auth [options] [label]` + +Generate and configure a fully functional authentication application for your project. Please refer to [Authentication](../../authentication.mdx) to learn more about authentication in Marten, and to [Generated files](../../authentication/reference/generated-files.md) to see a list of the files generated for the authentication app specifically. + +:::info +This generator will attempt to add the generated application to the [`installed_apps`](./settings.md#installed_apps) setting and will also configure Crystal requirements for it (in the `src/project.cr` and `src/cli.cr` files). It will also add authentication-related settings to your base settings file and will add the [`marten-auth`](https://github.com/martenframework/marten-auth) shard to your project's `shard.yml`. +::: + +### Arguments + +* `label` - Label of the authentication application to generate (default to "auth") + +### Examples + +```bash +marten gen auth # Generate a new authentication app with the 'auth' label +marten gen auth my_auth # Generate a new authentication app with the 'my_auth' label +``` + +## `email` + +**Usage:** `marten gen email [options] [name]` + +Generate an email. Please refer to [Emailing](../../emailing.mdx) to learn more about emailing in Marten. + +### Options + +* `--app=APP` - Target app where the email should be created (default to the [main app](../applications.md#the-main-application)) +* `--parent=PARENT` - Parent class name for the generated email + +### Arguments + +* `name` - Name of the email to generate (must be CamelCase) + +### Examples + +```bash +marten gen email TestEmail # Generate a new TestEmail email in the main application +marten gen email TestEmail --app blog # Generate a new TestEmail email in the blog application +``` + +## `handler` + +Generate a handler. Please refer to [Handlers](../../handlers-and-http/introduction.md) to learn more about handlers. + +### Options + +* `--app=APP` - Target app where the handler should be created (default to the [main app](../applications.md#the-main-application)) +* `--parent=PARENT` - Parent class name for the generated handler + +### Arguments + +* `name` - Name of the handler to generate (must be CamelCase) + +### Examples + +```bash +marten gen handler TestHandler # Generate a new TestHandler handler in the main application +marten gen handler TestHandler --app blog # Generate a new TestHandler handler in the blog application +``` + +## `model` + +Generate a model. Please refer to [Models](../../models-and-databases/introduction.md) to learn more about models. + +### Options + +* `--app=APP` - Target app where model handler should be created (default to the [main app](../applications.md#the-main-application)) +* `--parent=PARENT` - Parent class name for the generated model +* `--no-timestamps` - Do not include timestamp fields in the generated model + +### Arguments + +* `name` - Name of the model to generate (must be CamelCase) +* `field_definitions` - Field definitions of the model to generate + +### Details + +This generator can generate a model with the specified name and field definitions. The model is generated in the app specified by the `--app` option or in the [main app](../applications.md#the-main-application) if no app is specified. + +Field definitions can be specified using the following formats: + +``` +name:type +name:type{qualifier} +name:type:modifier:modifier +``` + +Where `name` is the name of the field and `type` is the type of the field. + +`qualifier` can be required depending on the considered field type; when this is the case, it corresponds to a mandatory field option. For example, `label:string{128}` will produce a [string field](../../models-and-databases/reference/fields.md#string) whose `max_size` option is set to `128`. Another example: `author:many_to_one{User}` will produce a [many-to-one field](../../models-and-databases/reference/fields.md#many_to_one) whose `to` option is set to target the `User` model. + +`modifier` is an optional field modifier. Field modifiers are used to specify additional (but non-mandatory) field options. For example: `name:string:uniq` will produce a [string field](../../models-and-databases/reference/fields.md#string) whose `unique` option is set to `true`. Another example: `name:string:uniq:index` will produce a [string field](../../models-and-databases/reference/fields.md#string) whose `unique` and `index` options are set to `true`. + +### Examples + +```bash +# Generate a model in the main app: +marten gen model User name:string email:string + +# Generate a model in the admin app: +marten gen model User name:string email:string --app admin + +# Generate a model with a many-to-one reference: +marten gen model Article label:string body:text author:many_to_one{User} + +# Generate a model with a parent class: +marten gen model Admin::User name:string email:string --parent User + +# Generate a model without timestamps: +marten gen model User name:string email:string --no-timestamps +``` + +## `schema` + +Generate a schema. Please refer to [Schemas](../../schemas/introduction.md) to learn more about schemas. + +### Options + +* `--app=APP` - Target app where schema should be created (default to the [main app](../applications.md#the-main-application)) +* `--parent=PARENT` - Parent class name for the generated schema + +### Arguments + +* `name` - Name of the schema to generate (must be CamelCase) +* `field_definitions` - Field definitions of the schema to generate + +### Details + +This generator can generate a schema with the specified name and field definitions. The schema is generated in the app specified by the `--app` option or in the [main app](../applications.md#the-main-application) if no app is specified. + +Field definitions can be specified using the following formats: + +``` +name:type +name:type:modifier:modifier +``` + +Where `name` is the name of the field and `type` is the type of the field. + +`modifier` is an optional field modifier. Field modifiers are used to specify additional (but non-mandatory) field options. For example: `name:string:optional` will produce a [string field](../../schemas/reference/fields.md#string) whose `required` option is set to `false`. + +### Examples + +```bash +# Generate a schema in the main app: +marten gen schema ArticleSchema title:string body:string + +# Generate a schema in the blog app: +marten gen schema ArticleSchema title:string body:string --app admin + +# Generate a schema with a parent class: +marten gen schema ArticleSchema title:string body:string --parent BaseSchema +``` + +## `secretkey` + +Generate a new secret key value that can be used in the [`secret_key`](./settings.md#secret_key) setting. + +### Examples + +```bash +marten gen secretkey +``` diff --git a/docs/versioned_docs/version-0.2/development/reference/management-commands.md b/docs/versioned_docs/version-0.5/development/reference/management-commands.md similarity index 62% rename from docs/versioned_docs/version-0.2/development/reference/management-commands.md rename to docs/versioned_docs/version-0.5/development/reference/management-commands.md index 5460cda6d..9ea5c2aff 100644 --- a/docs/versioned_docs/version-0.2/development/reference/management-commands.md +++ b/docs/versioned_docs/version-0.5/development/reference/management-commands.md @@ -6,6 +6,25 @@ toc_max_heading_level: 2 This page provides a reference for all the available management commands and their options. +## `clearsessions` + +**Usage:** `marten clearsessions [options]` + +Clears all expired sessions for the configured session store. + +Please refer to [Sessions](../../handlers-and-http/sessions.md) to learn more about sessions. + +### Options + +* `--no-input` - Does not show prompts to the user + +### Examples + +```bash +marten clearsessions # Clears all expired sessions +marten clearsessions --no-input # Clears all expired sessions without any prompts +``` + ## `collectassets` **Usage:** `marten collectassets [options]` @@ -17,6 +36,8 @@ Please refer to [Asset handling](../../assets/introduction.md) to learn more abo ### Options * `--no-input` - Does not show prompts to the user +* `--fingerprint` - Attaches a fingerprint to the collected assets +* `--manifest-path` - Configures where the manifest.json is stored. Only relevant if `--fingerprint` is activated. (default to `src/manifest.json`) ### Examples @@ -25,6 +46,39 @@ marten collectassets # Collects all the assets marten collectassets --no-input # Collects all the assets without any prompts ``` +## `gen` + +**Usage:** `marten gen [options] [generator] [arguments]` + +Generate various structures, abstractions, and values within an existing project. + +### Options + +Generators support their own specific options. For an exact list of generator options, please refer to the [Generators reference](./generators.md). + +### Arguments + +* `generator` - Name of the generator to use +* `arguments` - Generator-specific arguments + +Generators support their own specific arguments. For an exact list of generator arguments, please refer to the [Generators reference](./generators.md). + +### Examples + +```bash +marten gen secretkey # Generate a secret key value +marten gen email WelcomeEmail # Generate a WelcomeEmail email in the main application +marten gen handler MyHandler --app=blog # Generate a MyHandler handler in the blog application +``` + +:::tip +You can also use the alias `g` to execute specific generators: + +```bash +marten g model Test label:string:uniq +``` +::: + ## `genmigrations` **Usage:** `marten genmigrations [options] [app_label]` @@ -83,6 +137,7 @@ The `migrate` command allows you to apply (or unapply) migrations to your databa ### Options * `--fake` - Allows marking migrations as applied or unapplied without actually running them +* `--plan` - Provides a comprehensive overview of the operations that will be performed by the applied or unapplied migrations * `--db=ALIAS` - Allows specifying the alias of the database on which migrations will be applied or unapplied (default to `default`) ### Arguments @@ -102,9 +157,9 @@ marten migrate foo 202203111821451 # Applies (or unapply) migrations for the "fo **Usage:** `marten new [options] [type] [name]` -Initializes a new Marten project or application structure. +Initializes a new Marten project or application repository structure. -The `new` management command can be used to create either a new project structure or a new [application](../applications.md) structure. This can be handy when creating new projects or when introducing new applications into an existing project, as it ensures you are following Marten's best practices and conventions. +The `new` management command can be used to create either a new project repository or a new [application](../applications.md) repository. This can be handy when creating new projects, or when creating new applications that are intended to be distributed as dedicated shards, as it ensures you are following Marten's best practices and conventions. The command allows you to fully define the name of your project or application, and in which folder it should be created. @@ -112,18 +167,43 @@ The command allows you to fully define the name of your project or application, * `-d DIR, --dir=DIR` - An optional destination directory * `--with-auth` - Adds an authentication application to newly created projects. See [Authentication](../../authentication.mdx) to learn more about this capability +* `--database` - Preconfigures the application database. Currently `mysql`, `postgresq` and `sqlite3` are supported. See [Database settings](../../development/reference/settings.md#database-settings) for more information +* `-e, --edge` - Use the development version of Marten ### Arguments * `type` - The type of structure to create (must be either `project` or `app`) * `name` - The name of the project or app to create +:::tip +The `type` and `name` arguments are optional: if they are not provided, an interactive mode will be used and the command will prompt the user for inputting the structure type, the app or project name, and whether the auth app should be generated. +::: + ### Examples ```bash -marten new project myblog # Creates a "myblog" project +marten new project myblog # Creates a "myblog" project repository structure marten new project myblog --dir=./projects/myblog # Creates a "myblog" project in the "./projects/myblog" folder -marten new app auth # Creates an "auth" application +marten new app auth # Creates an "auth" application repository structure +``` + +## `play` + +**Usage:** `marten play [options]` + +Start a Crystal playground server initialized for the current project. + +### Options + +* `-b HOST, --bind=HOST` - Binds the playground to the specified IP +* `-p PORT, --port=PORT` - Runs the playground on the specified port +* `--open` - Open the playground in the default browser automatically + +### Examples + +```bash +marten play # Starts the Crystal playground using the default host/port +marten play --open # Starts the Crystal playground using the default host/port and opens it in the default browser ``` ## `resetmigrations` @@ -158,13 +238,23 @@ Starts a development server that is automatically recompiled when source files c * `-b HOST, --bind=HOST` - Allows specifying a custom host to bind * `-p PORT, --port=PORT` - Allows specifying a custom port to listen for connections +* `--open` - Open the server in the default browser automatically ### Examples ```bash marten serve # Starts a development server using the configured host and port marten serve -p 3000 # Starts a development server by overriding the port +marten serve --open # Starts a development server and opens it in the default browser +``` + +:::tip +You can also use the alias `s` to start the development server: + +```bash +marten s ``` +::: ## `version` diff --git a/docs/versioned_docs/version-0.2/development/reference/settings.md b/docs/versioned_docs/version-0.5/development/reference/settings.md similarity index 68% rename from docs/versioned_docs/version-0.2/development/reference/settings.md rename to docs/versioned_docs/version-0.5/development/reference/settings.md index 850688f67..2707accf0 100644 --- a/docs/versioned_docs/version-0.2/development/reference/settings.md +++ b/docs/versioned_docs/version-0.5/development/reference/settings.md @@ -24,6 +24,16 @@ It should be noted that this setting is automatically set to the following array [".localhost", "127.0.0.1", "[::1]"] ``` +### `cache_store` + +Default: `Marten::Cache::Store::Memory.new` + +The global cache store instance. + +This setting allows to configure the cache store returned by the [`Marten#cache`](pathname:///api/0.5/Marten.html#cache%3ACache%3A%3AStore%3A%3ABase-class-method) method (which can be used to perform low-level caching operations), and which is also leveraged for other caching features such as template fragment caching. Please refer to [Caching](../../caching.mdx) to learn more about the caching features provided by Marten. + +By default, the global cache store is set to be an in-memory cache (instance of [`Marten::Cache::Store::Memory`](pathname:///api/0.5/Marten/Cache/Store/Memory.html)). In test environments you might want to use the "null store" by assigning an instance of the [`Marten::Cache::Store::Null](pathname:///api/0.5/Marten/Cache/Store/Null.html) to this setting. Additional caching store shards are also maintained under the umbrella of the Marten project or by the community itself and can be used as part of your application depending on your caching requirements. These backends are listed in the [caching stores backend reference](../../caching/reference/stores.md). + ### `debug` Default: `false` @@ -42,7 +52,7 @@ The host the HTTP server running the application will be listening on. Default: `[] of Marten::Apps::Config.class` -An array of the installed app classes. Each Marten application must define a subclass of [`Marten::Apps::Config`](pathname:///api/0.2/Marten/Apps/Config.html). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. Please refer to [Applications](../applications.md) to learn more about applications. +An array of the installed app classes. Each Marten application must define a subclass of [`Marten::Apps::Config`](pathname:///api/0.5/Marten/Apps/Config.html). When those subclasses are specified in the `installed_apps` setting, the applications' models, migrations, assets, and templates will be made available to the considered project. Please refer to [Applications](../applications.md) to learn more about applications. ### `log_backend` @@ -56,6 +66,8 @@ Default: `Log::Severity::Info` The default log level used by the application. Any severity defined in the [`Log::Severity`](https://crystal-lang.org/api/Log/Severity.html) enum can be used. +The [development environment](../settings#environments) in new Marten projects is configured with the more verbose `Log::Severity::Debug` log level, as it provides valuable debugging information during development of the project. + ### `middleware` Default: `[] of Marten::Middleware.class` @@ -92,6 +104,20 @@ The maximum number of allowed parameters per request (such as GET or POST parame A large number of parameters will require more time to process and might be the sign of a denial-of-service attack, which is why this setting can be used. This protection can also be disabled by setting `request_max_parameters` to `nil`. +### `root_path` + +Default: `nil` + +The root path of the application. + +The root path of the application specifies the actual location of the project sources in your system. This can prove helpful in scenarios where the project was compiled in a specific location different from the final destination where the project sources (and the `lib` folder) are copied. For instance, platforms like Heroku often fall under this category. By configuring the root path, you can ensure that your application correctly locates the required project sources and avoids any discrepancies arising from inconsistent source paths. This can prevent issues related to missing dependencies or missing app-related files (eg. locales, assets, or templates) and make your application more robust and reliable. + +For example, deploying a Marten app on Heroku will usually involves setting the root path as follows: + +```crystal +config.root_path = "/app" +``` + ### `secret_key` Default: `""` @@ -110,6 +136,18 @@ Default: `Time::Location.load("UTC")` The default time zone used by the application when it comes to storing date times in the database and displaying them. Any [`Time::Location`](https://crystal-lang.org/api/Time/Location.html) object can be used. +### `trailing_slash` + +Default: `:do_nothing` + +The trailing slash behavior applied in case an incoming request URL does not match any of the configured routes. + +This setting allows you to configure whether an HTTP permanent redirect (301) should be issued when an incoming URL that does not match any of the configured routes either ends with a slash or does not. Three values are supported: + +* `:do_nothing` - No redirect is issued (this is the default behavior). +* `:add` - If the incoming URL does not end with a slash and does not match any routes, a redirect is issued to the same URL with a trailing slash appended. +* `:remove` - If the incoming URL ends with a slash and does not match any routes, a redirect is issued to the same URL with the trailing slash removed. + ### `use_x_forwarded_host` Default: `false` @@ -192,7 +230,13 @@ config.assets.dirs = [ Default: `[] of String` -An array of paths to manifest JSON files to use to resolve assets URLs. Manifest files will be used to return the right fingerprinted asset path for a generic path, which can be useful if your asset bundling strategy support this. +An array of paths to manifest JSON files to use to resolve assets URLs. Manifest files will be used to return the right fingerprinted asset path for a generic path, which can be useful if your asset bundling strategy support this. You can read more about this capability in [Asset manifests and fingerprinting](../../assets/introduction.md#asset-manifests-and-fingerprinting). + +### `max_age` + +Defaults: `3600` + +Allows to set the max-age directive value used as part of the Cache-Control header that is set by the [`Marten::Middleware::AssetServing`](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware. ### `root` @@ -208,9 +252,9 @@ This setting is only used if `assets.storage` is `nil`. Default: `nil` -An optional storage object, which must be an instance of a subclass of [`Marten::Core::Store::Base`](pathname:///api/0.2/Marten/Core/Storage/Base.html). This storage object will be used when collecting asset files to persist them in a given location. +An optional storage object, which must be an instance of a subclass of [`Marten::Core::Store::Base`](pathname:///api/0.5/Marten/Core/Storage/Base.html). This storage object will be used when collecting asset files to persist them in a given location. -By default this setting value is set to `nil`, which means that a [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage is automatically constructed by using the `assets.root` and `assets.url` setting values: in this situation, asset files are collected and persisted in a local directory, and it is expected that they will be served from this directory by the web server running the application. +By default this setting value is set to `nil`, which means that a [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage is automatically constructed by using the `assets.root` and `assets.url` setting values: in this situation, asset files are collected and persisted in a local directory, and it is expected that they will be served from this directory by the web server running the application. A specific storage can be set instead to ensure that collected assets are persisted somewhere else in the cloud and served from there (for example in an Amazon's S3 bucket). When this is the case, the `assets.root` and `assets.url` setting values are basically ignored and are overridden by the use of the specified storage. @@ -218,7 +262,7 @@ A specific storage can be set instead to ensure that collected assets are persis Default: `"/assets/"` -The base URL to use when exposing asset URLs. This base URL will be used by the default [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage to construct asset URLs. For example, requesting a `css/App.css` asset might generate a `/assets/css/App.css` URL by default. +The base URL to use when exposing asset URLs. This base URL will be used by the default [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage to construct asset URLs. For example, requesting a `css/App.css` asset might generate a `/assets/css/App.css` URL by default. :::info This setting is only used if `assets.storage` is `nil`. @@ -277,6 +321,16 @@ Default: `true` A boolean indicating if the CSRF protection is enabled globally. When set to `true`, handlers will automatically perform a CSRF check to protect unsafe requests (ie. requests whose methods are not `GET`, `HEAD`, `OPTIONS`, or `TRACE`). Regardless of the value of this setting, it is always possible to explicitly enable or disable CSRF protection on a per-handler basis. See [Cross-Site Request Forgery protection](../../security/csrf.md) for more details. +### `session_key` + +Default: `"csrftoken"` + +The name of the session key to use for the CSRF token. This session key should be different than any other session key created by your application. + +:::info +This value is only relevant if [`use_session`](#use_session) is set to `true`. +::: + ### `trusted_origins` Default: `[] of String` @@ -294,6 +348,59 @@ config.csrf.trusted_origins = [ ] ``` +### `use_session` + +Default: `false` + +A boolean indicating whether the CSRF token should be stored inside a session. +If set to `true`, the CSRF token will be stored [in a session](../../handlers-and-http/sessions.md) rather than in a cookie. + +## Content-Security-Policy settings + +These settings allow configuring how the [`Marten::Middleware::ContentSecurityPolicy`](../../handlers-and-http/reference/middlewares.md#content-security-policy-middleware) middleware behaves and the actual directives of the Content-Security-Policy header that are set by this middleware. + +```crystal +config.content_security_policy.report_only = true +config.content_security_policy.default_policy.default_src = [:self, "other"] +``` + +Please refer to [Content Security Policy](../../security/content-security-policy.md) to learn more about the Content-Security-Policy header protection. + +:::tip +[Content-Security-Policy](https://www.w3.org/TR/CSP/) is a complicated header and there are possibly many values you may need to tweak. Make sure you understand it before configuring the below settings. +::: + +### `default_policy` + +Default: `Marten::HTTP::ContentSecurityPolicy.new` + +The default Content-Security-Policy object. + +This [`Marten::HTTP::ContentSecurityPolicy`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html) object will be used to set the Content-Security-Policy header when the [`Marten::Middleware::ContentSecurityPolicy`](../../handlers-and-http/reference/middlewares.md#content-security-policy-middleware) middleware is used. + +All the attributes that can be set on this [`Marten::HTTP::ContentSecurityPolicy`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html) object through the use of methods such as [`#default_src=`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html#default_src%3D(value%3AArray|Nil|String|Symbol|Tuple)-instance-method) or [`#frame_src=`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html#frame_src%3D(value%3AArray|Nil|String|Symbol|Tuple)-instance-method) can also be used directly on the `content_security_policy` setting object. For example: + +```crystal +config.content_security_policy.default_src = [:self, "other"] +config.content_security_policy.block_all_mixed_content = true +``` + +### `nonce_directives` + +Default: `["script-src", "style-src"]` + +An array of directives where a dynamically-generated nonce will be included. + +For example, if this setting is set to `["script-src"]`, a `nonce-` value will be added to the `script-src` directive in the Content-Security-Policy header value. + +### `report_only` + +Default: `false` + +A boolean indicating whether policy violations are reported without enforcing them. + +If this setting is set to `true`, the [`Marten::Middleware::ContentSecurityPolicy`](../../handlers-and-http/reference/middlewares.md#content-security-policy-middleware) middleware will set a [Conten-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) header instead of the regular Content-Security-Policy header. Doing so can be useful to experiment with policies without enforcing them. + ## Database settings These settings allow configuring the databases used by the considered Marten project. At least one default database must be configured if your project makes use of [models](../../models-and-databases/introduction.md), and additional databases can optionally be configured as well. @@ -312,7 +419,7 @@ config.database :other do |db| db ``` -Configuring other database backends such as MySQL or PostgreSQL usually involves specifying more connection parameters (eg. user, password, etc). For example: +Configuring other database backends such as MariaDB, MySQL or PostgreSQL usually involves specifying more connection parameters (eg. user, password, etc). For example: ```crystal config.database do |db| @@ -372,6 +479,26 @@ Default: `nil` The name of the database to connect to. If you use the `sqlite` backend, this can be a string or a `Path` object containing the path (absolute or relative) to the considered database path. +### `options` + +Default: `{} of String => String` + +A set of additional database options. This setting can be used to set additional database options that may be required in order to connect to the database at hand. + +For example: + +```crystal +config.database do |db| + db.backend = :postgresql + db.host = "localhost" + db.name = "my_db" + db.user = "my_user" + db.password = "my_passport" + // highlight-next-line + db.options = {"sslmode" => "disable"} +end +``` + ### `password` Default: `nil` @@ -419,7 +546,7 @@ Default: `Marten::Emailing::Backend::Development.new` The backend to use when it comes to send emails. Emailing backends define _how_ emails are actually sent. -By default, a development backend (instance of [`Marten::Emailing::Backend::Dev`](pathname:///api/0.2/Marten/Emailing/Backend/Development.html)) is used: this backend "collects" all the emails that are "delivered" by default (which can be used in specs in order to test sent emails), but it can also be configured to print email details to the standard output if necessary (see the [emailing backend reference](../../emailing/reference/backends.md) for more details about this capability). +By default, a development backend (instance of [`Marten::Emailing::Backend::Dev`](pathname:///api/0.5/Marten/Emailing/Backend/Development.html)) is used: this backend "collects" all the emails that are "delivered" by default (which can be used in specs in order to test sent emails), but it can also be configured to print email details to the standard output if necessary (see the [emailing backend reference](../../emailing/reference/backends.md) for more details about this capability). Additional emailing backend shards are also maintained under the umbrella of the Marten project or by the community itself and can be used as part of your application depending on your specific email sending requirements. These backends are listed in the [emailing backend reference](../../emailing/reference/backends.md#other-backends). @@ -427,7 +554,7 @@ Additional emailing backend shards are also maintained under the umbrella of the Default: `"webmaster@localhost"` -The default from address used in emails. Email definitions that don't specify a "from" address explicitly will use this email address automatically for the sender email. It should be noted that this from email address can be defined as a string or as a [`Marten::Emailing::Address`](pathname:///api/0.2/Marten/Emailing/Address.html) object (which allows to specify the name AND the address of the sender email). +The default from address used in emails. Email definitions that don't specify a "from" address explicitly will use this email address automatically for the sender email. It should be noted that this from email address can be defined as a string or as a [`Marten::Emailing::Address`](pathname:///api/0.5/Marten/Emailing/Address.html) object (which allows to specify the name AND the address of the sender email). ## I18n settings @@ -459,6 +586,12 @@ Default: `"en"` The default locale used by the Marten project. +### `locale_cookie_name` + +Default: `"marten_locale"` + +The name of the cookie to use for saving the locale of the current user and activating the right locale (when the [`Marten::Middleware::I18n`](../../handlers-and-http/reference/middlewares.md#i18n-middleware) middleware is used). See [Internationalization](../../i18n/introduction.md) to learn more about this capability. + ## Media files settings Media files settings allow configuring how Marten should interact with [media files](../../files/managing-files.md). These settings are all available under the `media_files` namespace: @@ -482,9 +615,9 @@ This setting is only used if `media_files.storage` is `nil`. Default: `nil` -An optional storage object, which must be an instance of a subclass of [`Marten::Core::Store::Base`](pathname:///api/0.2/Marten/Core/Storage/Base.html). This storage object will be used when uploading files to persist them in a given location. +An optional storage object, which must be an instance of a subclass of [`Marten::Core::Store::Base`](pathname:///api/0.5/Marten/Core/Storage/Base.html). This storage object will be used when uploading files to persist them in a given location. -By default, this setting value is set to `nil`, which means that a [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage is automatically constructed by using the `media_files.root` and `media_files.url` setting values: in this situation, media files are persisted in a local directory, and it is expected that they will be served from this directory by the web server running the application. +By default, this setting value is set to `nil`, which means that a [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage is automatically constructed by using the `media_files.root` and `media_files.url` setting values: in this situation, media files are persisted in a local directory, and it is expected that they will be served from this directory by the web server running the application. A specific storage can be set instead to ensure that uploaded files are persisted somewhere else in the cloud and served from there (for example in an Amazon's S3 bucket). When this is the case, the `media_files.root` and `media_files.url` setting values are basically ignored and are overridden by the use of the specified storage. @@ -492,12 +625,40 @@ A specific storage can be set instead to ensure that uploaded files are persiste Default: `"/media/"` -The base URL to use when exposing media files URLs. This base URL will be used by the default [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage to construct media files URLs. For example, requesting a `foo/bar.txt` file might generate a `/media/foo/bar.txt` URL by default. +The base URL to use when exposing media files URLs. This base URL will be used by the default [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage to construct media files URLs. For example, requesting a `foo/bar.txt` file might generate a `/media/foo/bar.txt` URL by default. :::info This setting is only used if `media_files.storage` is `nil`. ::: +## Method overriding settings + +Method overriding settings allow configuring how the [`MethodOverride`](../../handlers-and-http/reference/middlewares.md#method-override-middleware) middleware should handle HTTP method overrides in forms. These settings are all available under the `method_override` namespace: + +```crystal +config.method_override.allowed_methods = ["DELETE", "PATCH", "PUT"] +config.method_override.http_header_name = "X-Http-Method-Override" +config.method_override.input_name = "_method" +``` + +### `allowed_methods` + +Default: `["DELETE", "PATCH", "PUT"]` + +An array of HTTP methods that are allowed to be overridden using the `input_name` mechanism. This provides a layer of control, preventing the usage of arbitrary HTTP methods in overrides. + +### `http_header_name` + +Default: `X-Http-Method-Override` + +The name of the HTTP header used to signal a method override. + +### `input_name` + +Default: `_method` + +The name of the form input field (or query parameter) used to signal a method override. + ## Sessions settings Sessions settings allow configuring how Marten should handle [sessions](../../handlers-and-http/introduction.md#using-sessions). These settings are all available under the `sessions` namespace: @@ -552,6 +713,27 @@ A string containing the identifier of the store used to handle sessions. By default, sessions are stored within a single cookie. Cookies have a 4K size limit, which is usually sufficient to persist things like a user ID and flash messages. Other stores can be implemented and leveraged to store sessions data; see [Sessions](../../handlers-and-http/sessions.md) for more details about this capability. +## SSL redirect settings + +SSL redirect settings allow to configure how Marten should redirect non-HTTPS requests to HTTPS when the [`Marten::Middleware::SSLRedirect`](../../handlers-and-http/reference/middlewares.md#ssl-redirect-middleware) middleware is used: + +```crystal +config.ssl_redirect.host = "example-redirect.com" +config.exempted_paths = [/^\/no-ssl\/$/] +``` + +### `exempted_paths` + +Default: `[] of Regex | String` + +Allows to set the array of paths that should be exempted from HTTPS redirects. Both strings and regexes are accepted. + +### `host` + +Default: `nil` + +Allows to set the host that should be used when redirecting non-HTTPS requests. If set to `nil`, the HTTPS redirect will be performed using the request's host. + ## Strict transport security policy settings Strict transport security policy settings allow to configure how Marten should set the HTTP Strict-Transport-Security response header when the [`Marten::Middleware::StrictTransportSecurity`](../../handlers-and-http/reference/middlewares.md#strict-transport-security-middleware) middleware is used: @@ -628,3 +810,25 @@ config.templates.dirs = [ :"src/path2/templates", ] ``` + +### `isolated_inclusions` + +Default: `false` + +A boolean option enabling or disabling isolated inclusions of templates when using the [`include`](../../templates/reference/tags.md#include) template tag. When set to `true`, included templates won't have access to variables from the outer context by default, unless the `contextual` modifier is used with the `include` template tag. Conversely, when set to `false`, included templates will have access to the outer context's variables by default, unless explicitly disabled with the `isolated` modifier of the `include` template tag. + +### `loaders` + +Default: `nil` + +Overwrites the loading mechanism of templates. Takes an array of classes inheriting from `Marten::Template::Loader::Base`. Customize this to load templates from various sources like databases or in-memory structures. For example, to load templates from a file system: + +```crystal +config.templates.loaders = [Marten::Template::Loader::FileSystem.new("/path/to/templates")] of Marten::Template::Loader::Base +``` + +### `strict_variables` + +Default: `false` + +A boolean allowing to enable or disable the [strict variables](../../templates/introduction.md#strict-variables) for templates. When this setting is set to `true`, unknown variables encountered in templates will result in [`Marten::Template::Errors::UnknownVariable`](pathname:///api/0.5/Marten/Template/Errors/UnknownVariable.html) exceptions to be raised. When set to `false`, unknown variables will simply be treated as `nil` values in templates. diff --git a/docs/versioned_docs/version-0.2/development/settings.md b/docs/versioned_docs/version-0.5/development/settings.md similarity index 88% rename from docs/versioned_docs/version-0.2/development/settings.md rename to docs/versioned_docs/version-0.5/development/settings.md index 56b2728b3..12a894de3 100644 --- a/docs/versioned_docs/version-0.2/development/settings.md +++ b/docs/versioned_docs/version-0.5/development/settings.md @@ -12,7 +12,7 @@ Settings will be usually defined under a `config/settings` folder at the root of In such configuration, you will usually define shared settings (settings that are shared across all your environments) in a dedicated settings file (eg. `config/settings/base.cr`) and other environment-specific settings in other files (eg. `config/settings/development.cr`). -To define settings, it is necessary to access the global Marten configuration object through the use of the [`Marten#configure`](pathname:///api/0.2/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. This method returns a [`Marten::Conf::GlobalSettings`](pathname:///api/0.2/Marten/Conf/GlobalSettings.html) object that you can use to define setting values. For example: +To define settings, it is necessary to access the global Marten configuration object through the use of the [`Marten#configure`](pathname:///api/0.5/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. This method returns a [`Marten::Conf::GlobalSettings`](pathname:///api/0.5/Marten/Conf/GlobalSettings.html) object that you can use to define setting values. For example: ```crystal Marten.configure do |config| @@ -37,7 +37,7 @@ Marten.configure do |config| end ``` -It should be noted that the [`Marten#configure`](pathname:///api/0.2/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method can be called with an additional argument to ensure that the underlying settings are defined for a specific environment only: +It should be noted that the [`Marten#configure`](pathname:///api/0.5/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method can be called with an additional argument to ensure that the underlying settings are defined for a specific environment only: ```crystal Marten.configure :development do |config| @@ -46,7 +46,7 @@ end ``` :::caution -You should avoid altering setting values outside of the configuration block provided by the [`Marten#configure`](pathname:///api/0.2/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. Most settings are "read" and applied when the Marten project is set up, that is before the server actually starts. Changing these setting values afterward won't produce any meaningful result. +You should avoid altering setting values outside of the configuration block provided by the [`Marten#configure`](pathname:///api/0.5/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. Most settings are "read" and applied when the Marten project is set up, that is before the server actually starts. Changing these setting values afterward won't produce any meaningful result. ::: ## Environments @@ -57,9 +57,9 @@ When creating new projects by using the [`new`](./reference/management-commands * Test (settings defined in `config/settings/test.cr`) * Production (settings defined in `config/settings/production.cr`) -When your application is running, Marten will rely on the `MARTEN_ENV` environment variable to determine the current environment. If this environment variable is not found, the environment will automatically default to `development`. The value you specify in the `MARTEN_ENV` environment variable must correspond to the argument you pass to the [`Marten#configure`](pathname:///api/0.2/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. +When your application is running, Marten will rely on the `MARTEN_ENV` environment variable to determine the current environment. If this environment variable is not found, the environment will automatically default to `development`. The value you specify in the `MARTEN_ENV` environment variable must correspond to the argument you pass to the [`Marten#configure`](pathname:///api/0.5/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. -It should be noted that the current environment can be retrieved through the use of the [`Marten#env`](pathname:///api/0.2/Marten.html#env-class-method) method, which returns a [`Marten::Conf::Env`](pathname:///api/0.2/Marten/Conf/Env.html) object. For example: +It should be noted that the current environment can be retrieved through the use of the [`Marten#env`](pathname:///api/0.5/Marten.html#env-class-method) method, which returns a [`Marten::Conf::Env`](pathname:///api/0.5/Marten/Conf/Env.html) object. For example: ```crystal Marten.env # => diff --git a/docs/versioned_docs/version-0.2/development/testing.md b/docs/versioned_docs/version-0.5/development/testing.md similarity index 93% rename from docs/versioned_docs/version-0.2/development/testing.md rename to docs/versioned_docs/version-0.5/development/testing.md index 0db74860b..72e596156 100644 --- a/docs/versioned_docs/version-0.2/development/testing.md +++ b/docs/versioned_docs/version-0.5/development/testing.md @@ -105,10 +105,10 @@ By leveraging the test client, you can easily simulate various requests (eg. GET #### A simple example -To use the test client, you can either initialize a [`Marten::Spec::Client`](pathname:///api/0.2/Marten/Spec/Client.html) object or make use of the per-spec test client that is provided by the [`Marten::Spec#client`](pathname:///api/0.2/Marten/Spec.html#client%3AClient-class-method) method. Initializing new [`Marten::Spec::Client`](pathname:///api/0.2/Marten/Spec.html#client%3AClient-class-method) objects allow you to set client-wide properties, like a default content type. +To use the test client, you can either initialize a [`Marten::Spec::Client`](pathname:///api/0.5/Marten/Spec/Client.html) object or make use of the per-spec test client that is provided by the [`Marten::Spec#client`](pathname:///api/0.5/Marten/Spec.html#client%3AClient-class-method) method. Initializing new [`Marten::Spec::Client`](pathname:///api/0.5/Marten/Spec.html#client%3AClient-class-method) objects allow you to set client-wide properties, like a default content type. :::info -Note that the client returned by the [`Marten::Spec#client`](pathname:///api/0.2/Marten/Spec.html#client%3AClient-class-method) method is memoized and is reset after _each_ spec execution. +Note that the client returned by the [`Marten::Spec#client`](pathname:///api/0.5/Marten/Spec.html#client%3AClient-class-method) method is memoized and is reset after _each_ spec execution. ::: Let's have a look at a simple way to use the test client and verify the corresponding responses: @@ -127,7 +127,7 @@ end ``` :::tip -In the above example we are simply specifying a "raw" path by hardcoding its value. In a real scenario, you will likely want to [resolve your handler URLs](../handlers-and-http/routing.md#reverse-url-resolutions) using the [`Marten::Routing::Map#reverse`](pathname:///api/0.2/Marten/Routing/Map.html#reverse(name%3AString|Symbol%2Cparams%3AHash(String|Symbol%2CParameter%3A%3ATypes))-instance-method) method of the main routes map (that way, you don't hardcode route paths in your specs). For example +In the above example we are simply specifying a "raw" path by hardcoding its value. In a real scenario, you will likely want to [resolve your handler URLs](../handlers-and-http/routing.md#reverse-url-resolutions) using the [`Marten::Routing::Map#reverse`](pathname:///api/0.5/Marten/Routing/Map.html#reverse(name%3AString|Symbol%2Cparams%3AHash(String|Symbol%2CParameter%3A%3ATypes))-instance-method) method of the main routes map (that way, you don't hardcode route paths in your specs). For example ```crystal url = Marten.routes.reverse("article_detail", pk: 42) @@ -135,12 +135,12 @@ response = Marten::Spec.client.get(url, query_params: {"foo" => "bar"}) ``` ::: -Here we are simply issuing a GET request (by leveraging the [`#get`](pathname:///api/0.2/Marten/Spec/Client.html#get(path%3AString%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method) test client method) and testing the obtained response. A few things can be noted: +Here we are simply issuing a GET request (by leveraging the [`#get`](pathname:///api/0.5/Marten/Spec/Client.html#get(path%3AString%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method) test client method) and testing the obtained response. A few things can be noted: * The test client does not require your project's server to be running: internally it uses a lightweight server handlers chain that ensures that your project's middlewares are applied and that the URL you requested is resolved and mapped to the right handler * Only the path to the handler needs to be specified when issuing requests (eg. `/foo/bar`) -Note that you can also issue other types of requests by leveraging methods like [`#post`](pathname:///api/0.2/Marten/Spec/Client.html#post(path%3AString%2Cdata%3AHash|NamedTuple|Nil|String%3Dnil%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method), [`#put`](pathname:///api/0.2/Marten/Spec/Client.html#put(path%3AString%2Cdata%3AHash|NamedTuple|Nil|String%3Dnil%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method), or [`#delete`](pathname:///api/0.2/Marten/Spec/Client.html#delete(path%3AString%2Cdata%3AHash|NamedTuple|Nil|String%3Dnil%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method). For example: +Note that you can also issue other types of requests by leveraging methods like [`#post`](pathname:///api/0.5/Marten/Spec/Client.html#post(path%3AString%2Cdata%3AHash|NamedTuple|Nil|String%3Dnil%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method), [`#put`](pathname:///api/0.5/Marten/Spec/Client.html#put(path%3AString%2Cdata%3AHash|NamedTuple|Nil|String%3Dnil%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method), or [`#delete`](pathname:///api/0.5/Marten/Spec/Client.html#delete(path%3AString%2Cdata%3AHash|NamedTuple|Nil|String%3Dnil%2Cquery_params%3AHash|NamedTuple|Nil%3Dnil%2Ccontent_type%3AString|Nil%3Dnil%2Cheaders%3AHash|NamedTuple|Nil%3Dnil%2Csecure%3Dfalse)%3AMarten%3A%3AHTTP%3A%3AResponse-instance-method). For example: ```crystal describe MySchemaHandler do @@ -156,12 +156,12 @@ end ``` :::info -By default, CSRF checks are disabled for requests issued by the test client. If for some reasons you need to ensure that those are enabled, you can initialize a [`Marten::Spec::Client`](pathname:///api/0.2/Marten/Spec/Client.html) object with `disable_request_forgery_protection: false`. +By default, CSRF checks are disabled for requests issued by the test client. If for some reasons you need to ensure that those are enabled, you can initialize a [`Marten::Spec::Client`](pathname:///api/0.5/Marten/Spec/Client.html) object with `disable_request_forgery_protection: false`. ::: #### Introspecting responses -Responses returned by the test client are instances of the standard [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) class. As such you can easily access response attributes such as the status code, the content and content type, cookies, and headers in your specs in order to verify that the expected response was returned by your handler. +Responses returned by the test client are instances of the standard [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) class. As such you can easily access response attributes such as the status code, the content and content type, cookies, and headers in your specs in order to verify that the expected response was returned by your handler. #### Exceptions @@ -169,9 +169,9 @@ It is important to note that exceptions raised in your handlers will be visible #### Session and cookies -Test clients are always stateful: if a handler sets a cookie in the returned response, then this cookie will be stored in the client's cookie store (available via the [`#cookies`](pathname:///api/0.2/Marten/Spec/Client.html#cookies-instance-method) method) and will be automatically sent for subsequent requests issued by the client. +Test clients are always stateful: if a handler sets a cookie in the returned response, then this cookie will be stored in the client's cookie store (available via the [`#cookies`](pathname:///api/0.5/Marten/Spec/Client.html#cookies-instance-method) method) and will be automatically sent for subsequent requests issued by the client. -The same goes for session values: such values can be set using the session store returned by the [`#sessions`](pathname:///api/0.2/Marten/Spec/Client.html#session-instance-method) client method. If you set session values in this store prior to any request, the matched handler will have access to them and the new values that are set by the handler will be available for further inspection once the response is returned. These session values are also maintained between requests issued by a single client. +The same goes for session values: such values can be set using the session store returned by the [`#sessions`](pathname:///api/0.5/Marten/Spec/Client.html#session-instance-method) client method. If you set session values in this store prior to any request, the matched handler will have access to them and the new values that are set by the handler will be available for further inspection once the response is returned. These session values are also maintained between requests issued by a single client. For example: @@ -232,7 +232,7 @@ Marten.configure :test do |config| end ``` -Doing so will ensure that all sent emails are "collected" for further inspection. You can easily retrieve collected emails by calling the [`Marten::Spec#delivered_emails`](pathname:///api/0.2/Marten/Spec.html#delivered_emails%3AArray(Emailing%3A%3AEmail)-class-method) method, which returns an array of [`Marten::Email`](pathname:///api/0.2/Marten/Emailing/Email.html) instances. For example: +Doing so will ensure that all sent emails are "collected" for further inspection. You can easily retrieve collected emails by calling the [`Marten::Spec#delivered_emails`](pathname:///api/0.5/Marten/Spec.html#delivered_emails%3AArray(Emailing%3A%3AEmail)-class-method) method, which returns an array of [`Marten::Email`](pathname:///api/0.5/Marten/Emailing/Email.html) instances. For example: ```crystal describe MyObject do diff --git a/docs/versioned_docs/version-0.2/emailing.mdx b/docs/versioned_docs/version-0.5/emailing.mdx similarity index 86% rename from docs/versioned_docs/version-0.2/emailing.mdx rename to docs/versioned_docs/version-0.5/emailing.mdx index b69abcd71..abf593bd8 100644 --- a/docs/versioned_docs/version-0.2/emailing.mdx +++ b/docs/versioned_docs/version-0.5/emailing.mdx @@ -12,6 +12,9 @@ Marten provides a convenient email definition mechanism that leverages templates
+
+ +
## How-to's diff --git a/docs/versioned_docs/version-0.5/emailing/callbacks.md b/docs/versioned_docs/version-0.5/emailing/callbacks.md new file mode 100644 index 000000000..8e3575f07 --- /dev/null +++ b/docs/versioned_docs/version-0.5/emailing/callbacks.md @@ -0,0 +1,111 @@ +--- +title: Email callbacks +description: Learn how to define email callbacks. +sidebar_label: Callbacks +--- + +Callbacks enable you to define logic that is triggered at different stages of an email's lifecycle. This document covers the available callbacks and introduces you to the associated API, which you can use to define hooks in your emails. + +## Overview + +As stated above, callbacks are methods that will be called when specific events occur for a specific email instance. They need to be registered explicitly in your email classes. + +Registering a callback is as simple as calling the right callback macro (eg. `#before_deliver`) with a symbol of the name of the method to call when the callback is executed. + +For example, the following email leverages the [`#after_deliver`](#after_deliver) callback in order to emit a specific StatsD metric: + +```crystal +require "statsd" + +statsd = Statsd::Client.new + +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + after_deliver :emit_delivered_welcome_email_metric + + def initialize(@user : User) + end + + private def emit_delivered_welcome_email_metric + statsd.increment "email.welcome.delivered", tags: ["app:myapp"] + end +end +``` + +## Available callbacks + +### `before_deliver` + +`before_deliver` callbacks are executed _before_ an email is delivered (as part of the email's [`#deliver`](pathname:///api/0.5/Marten/Emailing/Email.html#deliver-instance-method) method). For example, this capability can be leveraged to mutate the considered email instance before the actual email gets delivered: + +```crystal +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + before_deliver :set_header + + def initialize(@user : User) + end + + private def set_header + headers["X-Debug"] = "True" if Marten.env.staging? + end +end +``` + +### `after_deliver` + +`after_deliver` callbacks are executed _after_ an email is delivered (as part of the email's [`#deliver`](pathname:///api/0.5/Marten/Emailing/Email.html#deliver-instance-method) method). For example, such callbacks can be leveraged to increment email-specific metrics: + +```crystal +require "statsd" + +statsd = Statsd::Client.new + +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + after_deliver :emit_delivered_welcome_email_metric + + def initialize(@user : User) + end + + private def emit_delivered_welcome_email_metric + statsd.increment "email.welcome.delivered", tags: ["app:myapp"] + end +end +``` + +### `before_render` + +`before_render` callbacks are invoked prior to rendering a template when generating the HTML or text body of the email. This means that these callbacks are executed when calling either the [`#deliver`](pathname:///api/0.5/Marten/Emailing/Email.html#deliver-instance-method), [`#html_body`](pathname:///api/0.5/Marten/Emailing/Email.html#html_body%3AString|Nil-instance-method), or [`#text_body`](pathname:///api/0.5/Marten/Emailing/Email.html#text_body%3AString|Nil-instance-method) methods. + +Typically, these callbacks can be used to add new variables to the global email template context, in order to make them available to the template runtime. This can be useful if your email has some instance variables that you want to expose to your email template. For example: + +```crystal +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + before_render :prepare_context + + def initialize(@user : User) + end + + private def prepare_context + context[:user] = @user + end +end +``` diff --git a/docs/versioned_docs/version-0.2/emailing/how-to/create-custom-emailing-backends.md b/docs/versioned_docs/version-0.5/emailing/how-to/create-custom-emailing-backends.md similarity index 89% rename from docs/versioned_docs/version-0.2/emailing/how-to/create-custom-emailing-backends.md rename to docs/versioned_docs/version-0.5/emailing/how-to/create-custom-emailing-backends.md index c75a2d5ca..020b8a551 100644 --- a/docs/versioned_docs/version-0.2/emailing/how-to/create-custom-emailing-backends.md +++ b/docs/versioned_docs/version-0.5/emailing/how-to/create-custom-emailing-backends.md @@ -7,7 +7,7 @@ Marten lets you easily create custom [emailing backends](../introduction.md#emai ## Basic backend definition -Defining an emailing backend is as simple as creating a class that inherits from the [`Marten::Emailing::Backend::Base`](pathname:///api/0.2/Marten/Emailing/Backend/Base.html) abstract class and that implements a unique `#deliver` method. This method takes a single `email` argument (instance of [`Marten::Emailing::Email`](pathname:///api/0.2/Marten/Emailing/Email.html)), corresponding to the email to deliver. +Defining an emailing backend is as simple as creating a class that inherits from the [`Marten::Emailing::Backend::Base`](pathname:///api/0.5/Marten/Emailing/Backend/Base.html) abstract class and that implements a unique `#deliver` method. This method takes a single `email` argument (instance of [`Marten::Emailing::Email`](pathname:///api/0.5/Marten/Emailing/Email.html)), corresponding to the email to deliver. For example: diff --git a/docs/versioned_docs/version-0.2/emailing/introduction.md b/docs/versioned_docs/version-0.5/emailing/introduction.md similarity index 67% rename from docs/versioned_docs/version-0.2/emailing/introduction.md rename to docs/versioned_docs/version-0.5/emailing/introduction.md index 8051ffc4e..d07bbca4a 100644 --- a/docs/versioned_docs/version-0.2/emailing/introduction.md +++ b/docs/versioned_docs/version-0.5/emailing/introduction.md @@ -8,7 +8,7 @@ Marten lets you define emails in a very declarative way and gives you the abilit ## Email definition -Emails must be defined as subclasses of the [`Emailing::Email`](pathname:///api/0.2/Marten/Emailing/Email.html) abstract class and they usually live in an `emails` folder at the root of an application. These classes can define to which email addresses the email is sent (including CC or BCC addresses) and with which [templates](../templates.mdx) the body of the email (HTML or plain text) is rendered. +Emails must be defined as subclasses of the [`Emailing::Email`](pathname:///api/0.5/Marten/Emailing/Email.html) abstract class and they usually live in an `emails` folder at the root of an application. These classes can define to which email addresses the email is sent (including CC or BCC addresses) and with which [templates](../templates.mdx) the body of the email (HTML or plain text) is rendered. For example, the following snippet defines a simple email that is sent to a specific user's email address: @@ -25,10 +25,10 @@ end ``` :::info -It is not necessary to systematically specify the `from` email address with the [`#from`](pathname:///api/0.2/Marten/Emailing/Email.html#from(value)-macro) macro. Indeed, unless specified, the default "from" email address that is defined in the [`emailing.from_address`](../development/reference/settings.md#from_address) setting is automatically used. +It is not necessary to systematically specify the `from` email address with the [`#from`](pathname:///api/0.5/Marten/Emailing/Email.html#from(value)-macro) macro. Indeed, unless specified, the default "from" email address that is defined in the [`emailing.from_address`](../development/reference/settings.md#from_address) setting is automatically used. ::: -In the above snippet, a `WelcomeEmail` email class is defined by inheriting from the [`Emailing::Email`](pathname:///api/0.2/Marten/Emailing/Email.html) abstract class. This email is initialized with a hypothetical `User` record, and this user's email address is used as the recipient email (through the use of the [`#to`](pathname:///api/0.2/Marten/Emailing/Email.html#to(value)-macro) macro). Other email properties are also defined in the above snippet, such as the "from" email address ([`#from`](pathname:///api/0.2/Marten/Emailing/Email.html#from(value)-macro) macro) and the subject of the email ([`#subject`](pathname:///api/0.2/Marten/Emailing/Email.html#subject(value)-macro) macro). +In the above snippet, a `WelcomeEmail` email class is defined by inheriting from the [`Emailing::Email`](pathname:///api/0.5/Marten/Emailing/Email.html) abstract class. This email is initialized with a hypothetical `User` record, and this user's email address is used as the recipient email (through the use of the [`#to`](pathname:///api/0.5/Marten/Emailing/Email.html#to(value)-macro) macro). Other email properties are also defined in the above snippet, such as the "from" email address ([`#from`](pathname:///api/0.5/Marten/Emailing/Email.html#from(value)-macro) macro) and the subject of the email ([`#subject`](pathname:///api/0.5/Marten/Emailing/Email.html#subject(value)-macro) macro). ### Specifying email properties @@ -37,7 +37,7 @@ Most email properties (eg. from address, recipient addresses, etc) can be specif * through the use of a dedicated macro * by overriding a corresponding method in the email class -Indeed, it is convenient to define email properties through the use of the dedicated macros: [`#from`](pathname:///api/0.2/Marten/Emailing/Email.html#from(value)-macro) for the sender email, [`#to`](pathname:///api/0.2/Marten/Emailing/Email.html#to(value)-macro) for the recipient addresses, [`#cc`](pathname:///api/0.2/Marten/Emailing/Email.html#cc(value)-macro) for the CC addresses, [`#bcc`](pathname:///api/0.2/Marten/Emailing/Email.html#bcc(value)-macro) for the BCC addresses, [`#reply_to`](pathname:///api/0.2/Marten/Emailing/Email.html#reply_to(value)-macro) for the Reply-To address, and [`#subject`](pathname:///api/0.2/Marten/Emailing/Email.html#subject(value)-macro) for the email subject. +Indeed, it is convenient to define email properties through the use of the dedicated macros: [`#from`](pathname:///api/0.5/Marten/Emailing/Email.html#from(value)-macro) for the sender email, [`#to`](pathname:///api/0.5/Marten/Emailing/Email.html#to(value)-macro) for the recipient addresses, [`#cc`](pathname:///api/0.5/Marten/Emailing/Email.html#cc(value)-macro) for the CC addresses, [`#bcc`](pathname:///api/0.5/Marten/Emailing/Email.html#bcc(value)-macro) for the BCC addresses, [`#reply_to`](pathname:///api/0.5/Marten/Emailing/Email.html#reply_to(value)-macro) for the Reply-To address, and [`#subject`](pathname:///api/0.5/Marten/Emailing/Email.html#subject(value)-macro) for the email subject. That being said, if more complicated logics need to be implemented to generate these email properties, it is perfectly possible to simply override the corresponding method in the considered email class. For example: @@ -62,7 +62,7 @@ end ### Defining HTML and text bodies -The HTML body (and optionally text body) of the email is rendered using a [template](../templates.mdx) whose name can be specified by using the [`#template_name`](pathname:///api/0.2/Marten/Emailing/Email.html#template_name(template_name%3AString%3F%2Ccontent_type%3AContentType|String|Symbol%3DContentType%3A%3AHTML)%3ANil-class-method) class method. By default, unless explicitly specified, it is assumed that the template specified to this method is used for rendering the HTML body of the email. That being said, it is possible to explicitly specify for which content type the template should be used by specifying an optional `content_type` argument as follows: +The HTML body (and optionally text body) of the email is rendered using a [template](../templates.mdx) whose name can be specified by using the [`#template_name`](pathname:///api/0.5/Marten/Emailing/Email.html#template_name(template_name%3AString%3F%2Ccontent_type%3AContentType|String|Symbol%3DContentType%3A%3AHTML)%3ANil-class-method) class method. By default, unless explicitly specified, it is assumed that the template specified to this method is used for rendering the HTML body of the email. That being said, it is possible to explicitly specify for which content type the template should be used by specifying an optional `content_type` argument as follows: ```crystal class WelcomeEmail < Marten::Email @@ -79,9 +79,35 @@ end Note that it is perfectly valid to specify one template for rendering the HTML body AND another one for rendering the text body (like in the above example). :::info -Note that you can define [`#html_body`](pathname:///api/0.2/Marten/Emailing/Email.html#html_body%3AString%3F-instance-method) and [`#text_body`](pathname:///api/0.2/Marten/Emailing/Email.html#html_body%3AString%3F-instance-method) methods if you need to override the logic that allows generating the HTML or text body of your email. +Note that you can define [`#html_body`](pathname:///api/0.5/Marten/Emailing/Email.html#html_body%3AString%3F-instance-method) and [`#text_body`](pathname:///api/0.5/Marten/Emailing/Email.html#html_body%3AString%3F-instance-method) methods if you need to override the logic that allows generating the HTML or text body of your email. ::: +### Modifying the template context + +All emails have access to a [`#context`](pathname:///api/0.5/Marten/Emailing/Email.html#context-instance-method) method that returns a [template](../templates/introduction.md) context object. This "global" context object is available for the lifetime of the considered email and can be mutated in order to define which variables are made available to the template runtime when rendering templates in order to generate the HTML/text bodies of your email (which happens when [sending the email](#sending-emails)). + +To modify this context object effectively, it's recommended to utilize [`before_render`](./callbacks.md#before_render) callbacks, which are invoked just before rendering a template within your email. For example, this can be achieved as follows: + +```crystal +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + before_render :prepare_context + + def initialize(@user : User) + end + + private def prepare_context + context[:user] = @user + end +end +``` + +In the above example, the [`before_render`](./callbacks.md#before_render) callback simply assigns a new `user` variable to the email template context. Consequently, a corresponding `{{ user }}` variable will be available in the template configured for this email (`emails/welcome_email.html` in this case). + ### Defining custom headers If you need to insert custom headers into your emails, then you can easily do so by defining a `#headers` method in your email class. This method must return a hash of string keys and values. @@ -104,23 +130,23 @@ end ## Sending emails -Emails are sent _synchronously_ through the use of the [`#deliver`](pathname:///api/0.2/Marten/Emailing/Email.html#deliver-instance-method). For example, the `WelcomeEmail` email defined in the previous sections could be initialized and delivered by doing: +Emails are sent _synchronously_ through the use of the [`#deliver`](pathname:///api/0.5/Marten/Emailing/Email.html#deliver-instance-method). For example, the `WelcomeEmail` email defined in the previous sections could be initialized and delivered by doing: ```crystal email = WelcomeEmail.new(user) email.deliver ``` -When calling [`#deliver`](pathname:///api/0.2/Marten/Emailing/Email.html#deliver-instance-method), the considered email will be delivered by using the currently configured [emailing backend](#emailing-backends). +When calling [`#deliver`](pathname:///api/0.5/Marten/Emailing/Email.html#deliver-instance-method), the considered email will be delivered by using the currently configured [emailing backend](#emailing-backends). ## Emailing backends -Emailing backends define _how_ emails are actually sent when [`#deliver`](pathname:///api/0.2/Marten/Emailing/Email.html#deliver-instance-method) gets called. For example, a [development backend](./reference/backends.md#development-backend) might simply "collect" the sent emails and print their information to the standard output. Other backends might also integrate with existing email services or interact with an SMTP server to ensure email delivery. +Emailing backends define _how_ emails are actually sent when [`#deliver`](pathname:///api/0.5/Marten/Emailing/Email.html#deliver-instance-method) gets called. For example, a [development backend](./reference/backends.md#development-backend) might simply "collect" the sent emails and print their information to the standard output. Other backends might also integrate with existing email services or interact with an SMTP server to ensure email delivery. Which backend is used when sending emails is something that is controlled by the [`emailing.backend`](../development/reference/settings.md#backend-1) setting. All the available emailing backends are listed in the [emailing backend reference](./reference/backends.md). :::tip -If necessary, it is also possible to override which emailing backend is used on a per-email basis by leveraging the [`#backend`](pathname:///api/0.2/Marten/Emailing/Email.html#backend(backend%3ABackend%3A%3ABase)%3ANil-class-method) class method. For example: +If necessary, it is also possible to override which emailing backend is used on a per-email basis by leveraging the [`#backend`](pathname:///api/0.5/Marten/Emailing/Email.html#backend(backend%3ABackend%3A%3ABase)%3ANil-class-method) class method. For example: ```crystal class WelcomeEmail < Marten::Email @@ -136,3 +162,9 @@ class WelcomeEmail < Marten::Email end ``` ::: + +## Callbacks + +It is possible to define callbacks in order to bind methods and logics to specific events in the lifecycle of your emails. For example, it is possible to define callbacks that run before or after an email gets delivered. + +Please head over to the [Email callbacks](./callbacks.md) guide in order to learn more about email callbacks. diff --git a/docs/versioned_docs/version-0.2/emailing/reference/backends.md b/docs/versioned_docs/version-0.5/emailing/reference/backends.md similarity index 100% rename from docs/versioned_docs/version-0.2/emailing/reference/backends.md rename to docs/versioned_docs/version-0.5/emailing/reference/backends.md diff --git a/docs/versioned_docs/version-0.2/files.mdx b/docs/versioned_docs/version-0.5/files.mdx similarity index 100% rename from docs/versioned_docs/version-0.2/files.mdx rename to docs/versioned_docs/version-0.5/files.mdx diff --git a/docs/versioned_docs/version-0.2/files/how-to/create-custom-file-storages.md b/docs/versioned_docs/version-0.5/files/how-to/create-custom-file-storages.md similarity index 88% rename from docs/versioned_docs/version-0.2/files/how-to/create-custom-file-storages.md rename to docs/versioned_docs/version-0.5/files/how-to/create-custom-file-storages.md index 422e78b9b..933f5896f 100644 --- a/docs/versioned_docs/version-0.2/files/how-to/create-custom-file-storages.md +++ b/docs/versioned_docs/version-0.5/files/how-to/create-custom-file-storages.md @@ -7,14 +7,14 @@ Marten uses a file storage mechanism to perform file operations like saving file ## Basic file storage implementation -File storages are implemented as subclasses of the [`Marten::Core::Storage::Base`](pathname:///api/0.2/Marten/Core/Storage/Base.html) abstract class. As such, they must implement a set of mandatory methods which provide the following functionalities: - -* saving files ([`#save`](pathname:///api/0.2/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) -* deleting files ([`#delete`](pathname:///api/0.2/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) -* opening files ([`#open`](pathname:///api/0.2/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) -* verifying that files exist ([`#exist?`](pathname:///api/0.2/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) -* retrieving file sizes ([`#size`](pathname:///api/0.2/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) -* retrieving file URLs ([`#url`](pathname:///api/0.2/Marten/Core/Storage/Base.html#url(filepath%3AString)%3AString-instance-method)) +File storages are implemented as subclasses of the [`Marten::Core::Storage::Base`](pathname:///api/0.5/Marten/Core/Storage/Base.html) abstract class. As such, they must implement a set of mandatory methods which provide the following functionalities: + +* saving files ([`#save`](pathname:///api/0.5/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) +* deleting files ([`#delete`](pathname:///api/0.5/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) +* opening files ([`#open`](pathname:///api/0.5/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) +* verifying that files exist ([`#exist?`](pathname:///api/0.5/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) +* retrieving file sizes ([`#size`](pathname:///api/0.5/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) +* retrieving file URLs ([`#url`](pathname:///api/0.5/Marten/Core/Storage/Base.html#url(filepath%3AString)%3AString-instance-method)) Note that you can fully customize how file storage objects are initialized. diff --git a/docs/versioned_docs/version-0.2/files/managing-files.md b/docs/versioned_docs/version-0.5/files/managing-files.md similarity index 79% rename from docs/versioned_docs/version-0.2/files/managing-files.md rename to docs/versioned_docs/version-0.5/files/managing-files.md index 88c94ce72..3477c2f55 100644 --- a/docs/versioned_docs/version-0.2/files/managing-files.md +++ b/docs/versioned_docs/version-0.5/files/managing-files.md @@ -15,22 +15,22 @@ For example, let's consider the following model: ```crystal class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, blank: false, null: false + field :uploaded_file, :file, blank: false, null: false end ``` -Any `Attachment` model record will have a `file` attribute allowing interacting with the attached file: +Any `Attachment` model record will have an `uploaded_file` attribute allowing interacting with the attached file: ```crystal attachment = Attachment.first! -attachment.file # => # -attachment.file.attached? # => true -attachment.file.name # => "test.txt" -attachment.file.size # => 5796929 -attachment.file.url # => "/media/test.txt" +attachment.uploaded_file # => # +attachment.uploaded_file.attached? # => true +attachment.uploaded_file.name # => "test.txt" +attachment.uploaded_file.size # => 5796929 +attachment.uploaded_file.url # => "/media/test.txt" ``` -The object returned by the `Attachment#file` method is a "file object": an instance of [`Marten::DB::Field::File::File`](pathname:///api/0.2/Marten/DB/Field/File/File.html). These objects and their associated capabilities are described below in [File objects](#file-objects). +The object returned by the `Attachment#uploaded_file` method is a "file object": an instance of [`Marten::DB::Field::File::File`](pathname:///api/0.5/Marten/DB/Field/File/File.html). These objects and their associated capabilities are described below in [File objects](#file-objects). :::tip Under which path are files persisted? Files are stored at the root of the media [storage](#file-storages) by default. It should be noted that the path used to persist files in storages can be configured by setting the `upload_to` [`file`](../models-and-databases/reference/fields.md#file) field option. @@ -40,7 +40,7 @@ For example, the previous `Attachment` model could be rewritten as follows to en ```crystal class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, blank: false, null: false, upload_to: "foo/bar" + field :uploaded_file, :file, blank: false, null: false, upload_to: "foo/bar" end ``` @@ -49,7 +49,7 @@ It should also be noted that `upload_to` can correspond to a proc that takes the ```crystal class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, blank: false, null: false, upload_to: ->(name : String) { File.join("files/uploads", name) } + field :uploaded_file, :file, blank: false, null: false, upload_to: ->(name : String) { File.join("files/uploads", name) } end ``` ::: @@ -60,7 +60,7 @@ It should be noted that saving a model record will automatically result in any a attachment = Attachment.new File.open("test.txt") do |file| - attachment.file = file + attachment.uploaded_file = file attachment.save! end ``` @@ -71,21 +71,21 @@ You don't need to take care of possible collisions between attached file names: ## File objects -As mentioned previously, file objects are used internally by Marten to allow interacting with files that are associated with model records. These objects are instances of the [`Marten::DB::Field::File::File`](pathname:///api/0.2/Marten/DB/Field/File/File.html) class. They give access to basic file properties and they allow to interact with the associated IO. +As mentioned previously, file objects are used internally by Marten to allow interacting with files that are associated with model records. These objects are instances of the [`Marten::DB::Field::File::File`](pathname:///api/0.5/Marten/DB/Field/File/File.html) class. They give access to basic file properties and they allow to interact with the associated IO. It should be noted that these "file objects" are **always** associated with a model record (persisted or not), and as such, they are only used in the context of the [`file`](../models-and-databases/reference/fields.md#file) model field. Finally, it's worth mentioning that file objects can be **attached** and/or **committed**: -* an **attached** file object has an associated file set: in that case, its [`#attached?`](pathname:///api/0.2/Marten/DB/Field/File/File.html#attached%3F-instance-method) method returns `true` -* a **committed** file object has an associated file that is _persisted_ to the underlying [storage](#file-storages): in that case, its [`#committed?`](pathname:///api/0.2/Marten/DB/Field/File/File.html#committed%3F%3ABool-instance-method) method returns `true` +* an **attached** file object has an associated file set: in that case, its [`#attached?`](pathname:///api/0.5/Marten/DB/Field/File/File.html#attached%3F-instance-method) method returns `true` +* a **committed** file object has an associated file that is _persisted_ to the underlying [storage](#file-storages): in that case, its [`#committed?`](pathname:///api/0.5/Marten/DB/Field/File/File.html#committed%3F%3ABool-instance-method) method returns `true` For example: ```crystal attachment = Attachment.last! -attachment.file.attached? # => true -attachment.file.committed? # => true +attachment.uploaded_file.attached? # => true +attachment.uploaded_file.committed? # => true ``` ### Accessing file properties @@ -94,26 +94,26 @@ File objects give access to basic file properties through the use of the followi | Method | Description | | ----------- | ----------- | -| `#file` | Returns the associated / "wrapped" file object. This can be a real [`File`](https://crystal-lang.org/api/File.html) object, an uploaded file (instance of [`Marten::HTTP::UploadedFile`](pathname:///api/0.2/Marten/HTTP/UploadedFile.html)), or `nil` if no file is associated yet. | +| `#file` | Returns the associated / "wrapped" file object. This can be a real [`File`](https://crystal-lang.org/api/File.html) object, an uploaded file (instance of [`Marten::HTTP::UploadedFile`](pathname:///api/0.5/Marten/HTTP/UploadedFile.html)), or `nil` if no file is associated yet. | | `#name` | Returns the name of the file. | | `#size` | Returns the size of the file, using the associated [storage](#file-storages). | | `#url` | Returns the URL of the file, using the associated [storage](#file-storages). | ### Accessing the underlying file content -File objects allow you to access the underlying file content through the use of the [`#open`](pathname:///api/0.2/Marten/DB/Field/File/File.html#open%3AIO-instance-method) method. This method returns an [`IO`](https://crystal-lang.org/api/IO.html) object. +File objects allow you to access the underlying file content through the use of the [`#open`](pathname:///api/0.5/Marten/DB/Field/File/File.html#open%3AIO-instance-method) method. This method returns an [`IO`](https://crystal-lang.org/api/IO.html) object. For example: ```crystal attachment = Attachment.last! -file_io = attachment.file.open +file_io = attachment.uploaded_file.open puts file_io.gets_to_end ``` ### Updating the attached file -It is possible to update the actual file of a "file object" by using the [`#save`](pathname:///api/0.2/Marten/DB/Field/File/File.html#save(filepath%3A%3A%3AString%2Ccontent%3AIO%2Csave%3Dfalse)%3ANil-instance-method) method. This method allows saving the content of a specified [`IO`](https://crystal-lang.org/api/IO.html) object and associating it with a specific file path in the underlying [storage](#file-storages). +It is possible to update the actual file of a "file object" by using the [`#save`](pathname:///api/0.5/Marten/DB/Field/File/File.html#save(filepath%3A%3A%3AString%2Ccontent%3AIO%2Csave%3Dfalse)%3ANil-instance-method) method. This method allows saving the content of a specified [`IO`](https://crystal-lang.org/api/IO.html) object and associating it with a specific file path in the underlying [storage](#file-storages). For example: @@ -121,42 +121,42 @@ For example: attachment = Attachment.new File.open("test.txt") do |file| - attachment.file.save("path/to/test.txt", file) + attachment.uploaded_file.save("path/to/test.txt", file) attachment.save! end -attachment.file.url # => "/media/path/to/test.txt" +attachment.uploaded_file.url # => "/media/path/to/test.txt" ``` ### Deleting the attached file -It is also possible to manually "delete" the file associated with the "file object". To do so, the [`#delete`](pathname:///api/0.2/Marten/DB/Field/File/File.html#delete(save%3Dfalse)%3ANil-instance-method) method can be used. It should be noted that calling this method will remove the association between the model record and the file AND will also delete the file in the considered [storage](#file-storages). +It is also possible to manually "delete" the file associated with the "file object". To do so, the [`#delete`](pathname:///api/0.5/Marten/DB/Field/File/File.html#delete(save%3Dfalse)%3ANil-instance-method) method can be used. It should be noted that calling this method will remove the association between the model record and the file AND will also delete the file in the considered [storage](#file-storages). For example: ```crystal attachment = Attachment.last! -attachment.file.delete -attachment.file.attached? # => false -attachment.file.committed? # => false +attachment.uploaded_file.delete +attachment.uploaded_file.attached? # => false +attachment.uploaded_file.committed? # => false ``` ## File storages Marten uses a file storage mechanism to perform file operations like saving files, deleting files, generating URLs, ... This file storages mechanism allows to save files in different backends by leveraging a standardized API (eg. in the local file system, in a cloud bucket, etc). -By default, [`file`](../models-and-databases/reference/fields.md#file) model fields make use of the configured "media" storage. This storage uses the [`settings.media_files`](../development/reference/settings.md#media-files-settings) settings to determine what storage backend to use, and where to persist files. By default, the media storage uses the [`Marten::Core::Store::FileSystem`](pathname:///api/0.2/Marten/Core/Storage/FileSystem.html) storage backend, which ensures that files are persisted in the local file system, where the Marten application is running. +By default, [`file`](../models-and-databases/reference/fields.md#file) model fields make use of the configured "media" storage. This storage uses the [`settings.media_files`](../development/reference/settings.md#media-files-settings) settings to determine what storage backend to use, and where to persist files. By default, the media storage uses the [`Marten::Core::Store::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html) storage backend, which ensures that files are persisted in the local file system, where the Marten application is running. ### Interacting with the media file storage -You won't usually need to interact directly with the file storage, but it's worth mentioning that storage objects share the same API. Indeed, the class of these storage objects must inherit from the [`Marten::Core::Storage::Base`](pathname:///api/0.2/Marten/Core/Storage/Base.html) abstract class and implement a set of mandatory methods which provide the following functionalities: +You won't usually need to interact directly with the file storage, but it's worth mentioning that storage objects share the same API. Indeed, the class of these storage objects must inherit from the [`Marten::Core::Storage::Base`](pathname:///api/0.5/Marten/Core/Storage/Base.html) abstract class and implement a set of mandatory methods which provide the following functionalities: -* saving files ([`#save`](pathname:///api/0.2/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) -* deleting files ([`#delete`](pathname:///api/0.2/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) -* opening files ([`#open`](pathname:///api/0.2/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) -* verifying that files exist ([`#exist?`](pathname:///api/0.2/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) -* retrieving file sizes ([`#size`](pathname:///api/0.2/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) -* retrieving file URLs ([`#url`](pathname:///api/0.2/Marten/Core/Storage/Base.html#url(filepath%3AString)%3AString-instance-method)) +* saving files ([`#save`](pathname:///api/0.5/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) +* deleting files ([`#delete`](pathname:///api/0.5/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) +* opening files ([`#open`](pathname:///api/0.5/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) +* verifying that files exist ([`#exist?`](pathname:///api/0.5/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) +* retrieving file sizes ([`#size`](pathname:///api/0.5/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) +* retrieving file URLs ([`#url`](pathname:///api/0.5/Marten/Core/Storage/Base.html#url(filepath%3AString)%3AString-instance-method)) These capabilities are highlighted with the following example, where the media storage is used to interact with files: @@ -199,7 +199,7 @@ custom_storage = Marten::Core::Storage::FileSystem.new(root: "/tmp", base_url: " class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, blank: false, null: false, storage: custom_storage + field :uploaded_file, :file, blank: false, null: false, storage: custom_storage end ``` @@ -207,7 +207,7 @@ When doing this, all the file operations will be done using the configured stora ## Serving uploaded files during development -Marten provides a handler that you can use to serve media files in development environments only. This handler ([`Marten::Handlers::Defaults::Development::ServeMediaFile`](pathname:///api/0.2/Marten/Handlers/Defaults/Development/ServeMediaFile.html)) is automatically mapped to a route when creating new projects through the use of the [`new`](../development/reference/management-commands.md#new) management command: +Marten provides a handler that you can use to serve media files in development environments only. This handler ([`Marten::Handlers::Defaults::Development::ServeMediaFile`](pathname:///api/0.5/Marten/Handlers/Defaults/Development/ServeMediaFile.html)) is automatically mapped to a route when creating new projects through the use of the [`new`](../development/reference/management-commands.md#new) management command: ```crystal Marten.routes.draw do @@ -222,5 +222,5 @@ end As you can see, this route will automatically use the URL that is configured as part of the [`url`](../development/reference/settings.md#url-1) media files setting. For example, this means that a `foo/bar.txt` media file would be served by the `/media/foo/bar.txt` route in development if the [`url`](../development/reference/settings.md#url-1) setting is set to `/media/`. :::warning -It is very important to understand that this handler should **only** be used in development environments. Indeed, the [`Marten::Handlers::Defaults::Development::ServeMediaFile`](pathname:///api/0.2/Marten/Handlers/Defaults/Development/ServeMediaFile.html) handler is not suited for production environments as it is not really efficient or secure. A better way to serve uploaded files is to leverage a web server or a cloud bucket for example (depending on the configured media files storage). +It is very important to understand that this handler should **only** be used in development environments. Indeed, the [`Marten::Handlers::Defaults::Development::ServeMediaFile`](pathname:///api/0.5/Marten/Handlers/Defaults/Development/ServeMediaFile.html) handler is not suited for production environments as it is not really efficient or secure. A better way to serve uploaded files is to leverage a web server or a cloud bucket for example (depending on the configured media files storage). ::: diff --git a/docs/versioned_docs/version-0.2/files/uploading-files.md b/docs/versioned_docs/version-0.5/files/uploading-files.md similarity index 81% rename from docs/versioned_docs/version-0.2/files/uploading-files.md rename to docs/versioned_docs/version-0.5/files/uploading-files.md index 02ff8a97f..6d42ca875 100644 --- a/docs/versioned_docs/version-0.2/files/uploading-files.md +++ b/docs/versioned_docs/version-0.5/files/uploading-files.md @@ -8,20 +8,20 @@ Marten gives you the ability to interact with uploaded files. These files are ma ## Accessing uploaded files -Uploaded files are made available in the [`#data`](pathname:///api/0.2/Marten/HTTP/Request.html#data%3AParams%3A%3AData-instance-method) hash-like object of any HTTP request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html)). These file objects are instances of the [`Marten::HTTP::UploadedFile`](pathname:///api/0.2/Marten/HTTP/UploadedFile.html) class. +Uploaded files are made available in the [`#data`](pathname:///api/0.5/Marten/HTTP/Request.html#data%3AParams%3A%3AData-instance-method) hash-like object of any HTTP request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html)). These file objects are instances of the [`Marten::HTTP::UploadedFile`](pathname:///api/0.5/Marten/HTTP/UploadedFile.html) class. -For example, you could access and process a `file` file originating from an HTML form using a handler like this: +For example, you could access and process an `uploaded_file` file originating from an HTML form using a handler like this: ```crystal class ProcessUploadedFileHandler < Marten::Handler def post - file = request.data["file"].as(Marten::HTTP::UploadedFile) + file = request.data["uploaded_file"].as(Marten::HTTP::UploadedFile) respond "Processed file: #{file.filename}" end end ``` -[`Marten::HTTP::UploadedFile`](pathname:///api/0.2/Marten/HTTP/UploadedFile.html) objects give you access to the following key methods, which allow you to interact with the uploaded file and its content: +[`Marten::HTTP::UploadedFile`](pathname:///api/0.5/Marten/HTTP/UploadedFile.html) objects give you access to the following key methods, which allow you to interact with the uploaded file and its content: * `#filename` returns the name of the uploaded file * `#size` returns the size of the uploaded file @@ -39,7 +39,7 @@ For example, you could define the following schema: ```crystal class UploadFileSchema < Marten::Schema - field :file, :file + field :uploaded_file, :file end ``` @@ -52,7 +52,7 @@ class UploadFileHandler < Marten::Handlers::Schema success_url "/" def process_valid_schema - file = schema.validated_data["file"] + file = schema.validated_data["uploaded_file"] # Do something with the uploaded file... super @@ -75,13 +75,13 @@ class UploadFileHandler < Marten::Handlers::Schema success_url "/" def process_valid_schema - file = schema.validated_data["file"] + file = schema.validated_data["uploaded_file"] // highlight-next-line - Attachment.create!(file: file) + Attachment.create!(uploaded_file: file) super end end ``` -Here, the `UploadFileHandler` inherits from the [`Marten::Handlers::Schema`](pathname:///api/0.2/Marten/Handlers/Schema.html) generic handler. It would also make sense to leverage the [`Marten::Handlers::RecordCreate`](pathname:///api/0.2/Marten/Handlers/RecordCreate.html) generic handler to process the schema and create the `Attachment` record at the same time. +Here, the `UploadFileHandler` inherits from the [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) generic handler. It would also make sense to leverage the [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) generic handler to process the schema and create the `Attachment` record at the same time. diff --git a/docs/versioned_docs/version-0.2/getting-started.mdx b/docs/versioned_docs/version-0.5/getting-started.mdx similarity index 100% rename from docs/versioned_docs/version-0.2/getting-started.mdx rename to docs/versioned_docs/version-0.5/getting-started.mdx diff --git a/docs/versioned_docs/version-0.2/getting-started/installation.md b/docs/versioned_docs/version-0.5/getting-started/installation.md similarity index 59% rename from docs/versioned_docs/version-0.2/getting-started/installation.md rename to docs/versioned_docs/version-0.5/getting-started/installation.md index 7a54ddd34..b7871a735 100644 --- a/docs/versioned_docs/version-0.2/getting-started/installation.md +++ b/docs/versioned_docs/version-0.5/getting-started/installation.md @@ -3,7 +3,6 @@ title: Installation description: Get started by installing Marten and its dependencies. --- - This guide will help you get started in order to install Marten and its dependencies. Let's get started! ## Install Crystal @@ -26,21 +25,26 @@ On Ubuntu, Debian or any other Linux distribution using the APT package manager, curl -fsSL https://crystal-lang.org/install.sh | sudo bash ``` +### Using pacman + +On ArchLinux and derivates you can install Crystal and the `shards` command line tool through Pacman: + +```bash +sudo pacman -S crystal shards +``` + ## Install a database -New Marten projects will use a SQLite database by default: this lightweight serverless database application is usually already pre-installed on most of the existing operating systems, which makes it an ideal candidate for a development or a testing database. As such, if you choose to use SQLite for your new Marten project, you can very probably skip this section. +Marten officially supports **MariaDB**, **MySQL**, **PostgreSQL**, and **SQLite3** databases. New Marten projects will use a SQLite database by default: this lightweight serverless database application is usually already pre-installed on most of the existing operating systems, which makes it an ideal candidate for a development or a testing database. As such, if you choose to use SQLite for your new Marten project, you can very probably skip this section. -Marten also has built-in support for PostgreSQL and MySQL. Please refer to the applicable official documentation to install your database of choice: +Marten also has built-in support for PostgreSQL, MariaDB, and MySQL. Please refer to the applicable official documentation to install your database of choice: * [PostgreSQL Installation Guide](https://wiki.postgresql.org/wiki/Detailed_installation_guides) +* [MariaDB Installation Guide](https://mariadb.com/kb/en/getting-installing-and-upgrading-mariadb) * [MySQL Installation Guide](https://dev.mysql.com/doc/refman/8.0/en/installing.html) * [SQLite Installation Guide](https://www.tutorialspoint.com/sqlite/sqlite_installation.htm) -Each database requires the use of a dedicated shard (package of Crystal code). You don't have to install any of these right now if you are just starting with the framework or if you are planning to follow the [tutorial](./tutorial.md), but you may have to add one of the following entries to your project's `shard.yml` file later: - -* [crystal-pg](https://github.com/will/crystal-pg) (needed when using PostgreSQL databases) -* [crystal-mysql](https://github.com/crystal-lang/crystal-mysql) (needed when using MySQL databases) -* [crystal-sqlite3](https://github.com/crystal-lang/crystal-sqlite3) (needed when using SQLite3 databases) +Each database necessitates the use of a dedicated shard (a package of Crystal code). If you're just beginning with the framework or planning to follow the [tutorial](./tutorial.md), there's no immediate need to install these shards. However, if you intend to employ other databases like MariaDB, MySQL, or PostgreSQL, you may need to install database-specific shards. You can find instructions on how to do this in the [Configure database backends](../development/how-to/configure-database-backends.md) section. ## Install Marten @@ -55,6 +59,14 @@ brew tap martenframework/marten brew install marten ``` +### Using AUR on ArchLinux and derivates + +Assuming you use some AUR helper (`yay` in this example) it will be as simple as: + +```bash +yay -S marten +``` + Once the installation is complete, you should be able to use the `marten` command: ```bash @@ -66,7 +78,7 @@ marten -v Marten can be installed from the sources by running the following commands: ```bash -git clone --branch v0.2.x https://github.com/martenframework/marten +git clone https://github.com/martenframework/marten cd marten shards install crystal build src/marten_cli.cr -o bin/marten diff --git a/docs/versioned_docs/version-0.2/getting-started/tutorial.md b/docs/versioned_docs/version-0.5/getting-started/tutorial.md similarity index 95% rename from docs/versioned_docs/version-0.2/getting-started/tutorial.md rename to docs/versioned_docs/version-0.5/getting-started/tutorial.md index 5a7cca277..27315d5fc 100644 --- a/docs/versioned_docs/version-0.2/getting-started/tutorial.md +++ b/docs/versioned_docs/version-0.5/getting-started/tutorial.md @@ -45,6 +45,7 @@ myblog/ │   └── spec_helper.cr ├── src │   ├── assets +│   ├── emails │   ├── handlers │   ├── migrations │   ├── models @@ -53,6 +54,8 @@ myblog/ │   ├── cli.cr │   ├── project.cr │   └── server.cr +├── .editorconfig +├── .gitignore ├── manage.cr └── shard.yml ``` @@ -63,7 +66,8 @@ These files and folders are described below: | ----------- | ----------- | | config/ | Contains the configuration of the project. This includes environment-specific Marten configuration settings, initializers, and web application routes. | | spec/ | Contains the project specs, allowing you to test your application. | -| src/ | Contains the source code of the application. By default this folder will include a `project.cr` file (where all dependencies - including Marten itself - are required), a `server.cr` file (which starts the Marten web server), a `cli.cr` file (where migrations and CLI-related abstractions are required), and `assets`, `handlers`, `migrations`, `models`, `schemas`, and `templates` folders. | +| src/ | Contains the source code of the application. By default this folder will include a `project.cr` file (where all dependencies - including Marten itself - are required), a `server.cr` file (which starts the Marten web server), a `cli.cr` file (where migrations and CLI-related abstractions are required), and `assets`, `emails`, `handlers`, `migrations`, `models`, `schemas`, and `templates` folders. | +| .editorconfig | Regular `.editorconfig` which file defines basic indentation coding styles for Crystal. | | .gitignore | Regular `.gitignore` file which tells git the files and directories that should be ignored. | | manage.cr | This file defines a CLI that lets you interact with your Marten project in order to perform various actions (e.g. running database migrations, collecting assets, etc). | | shard.yml | The standard [shard.yml](https://crystal-lang.org/reference/the_shards_command/index.html) file, that lists the dependencies that are required to build your application. | @@ -228,24 +232,28 @@ config.database do |db| end ``` -An SQLite database is a good choice in order to try Marten and experiment with it (since SQLite is already pre-installed on most systems). That being said, if you need to use another database backend (for example, PostgreSQL or MySQL), feel free to have a look at the [databases configuration reference](../development/reference/settings.md#database-settings). +An SQLite database is a good choice in order to try Marten and experiment with it (since SQLite is already pre-installed on most systems). That being said, if you need to use another database backend (for example, PostgreSQL, MariaDB, or MySQL), feel free to have a look at the [databases configuration reference](../development/reference/settings.md#database-settings). ::: ## Interacting with model records Now that our `Article` model table has been created at the database level, let's try to make use of the Marten ORM to create and query article records. -To do so, we can launch the [Crystal playground](https://crystal-lang.org/reference/master/using_the_compiler/index.html#crystal-play) as follows: +To do so, we can launch an instance of the [Crystal playground](https://crystal-lang.org/reference/master/using_the_compiler/index.html#crystal-play) as follows: ```shell -crystal play +marten play ``` -You should be able to navigate to [http://localhost:8080](http://localhost:8080) and see a Crystal editor. Once you are there, replace the content of the live editor with the following: +You should be able to navigate to [http://localhost:8080](http://localhost:8080) and see a Crystal editor containing the following snippet: ```crystal require "./src/project" + +# Setup the project. Marten.setup + +# Write your code here. ``` These lines basically require your project dependencies and ensure that Marten is properly set up. You should keep those in the editor when playing with the following examples. Each of the following snippets is assumed to be copied/pasted below the previous one. The output of these examples is highlighted next to the `# =>` comment line. @@ -284,10 +292,10 @@ Now we can try to retrieve all the `Article` records that we currently have in ```crystal Article.all -# => ]> +# => ]> ``` -This method returns a `Marten::DB::Query::Set` object, which is commonly referred to as a "query set". A query set is a representation of records collections from the database that can be filtered, and iterated over. +This method returns an `Article::QuerySet` object, which is commonly referred to as a "query set". A query set is a representation of records collections from the database that can be filtered, and iterated over. The `Article::QuerySet` class, automatically generated for the `Article` model, is a subclass of `Marten::DB::Query::Set`. :::info Please refer to [Queries](../models-and-databases/queries.md) to learn more about Marten's querying capabilities. @@ -467,7 +475,7 @@ class ArticleCreateHandler < Marten::Handler redirect(reverse("home")) else - render("article_create.html", context: { schema: schema }) + render("article_create.html", context: { schema: schema }, status: 422) end end @@ -576,7 +584,7 @@ class ArticleUpdateHandler < Marten::Handler article.update!(schema.validated_data) redirect(reverse("home")) else - render("article_update.html", context: { article: article, schema: schema }) + render("article_update.html", context: { article: article, schema: schema }, status: 422) end end @@ -755,7 +763,7 @@ We can also add a link somewhere in the home page of the application to be able ## Refactoring: using template partials -The templates used for creating and updating an article look the same: they both make use of the same schema in order to create or update articles. It would be interesting to be able to reuse this form for both templates. This is where template "partials" cames in handy: these are template snippets that can be easily "included" into other templates to avoid duplications of code. +The templates used for creating and updating an article look the same: they both make use of the same schema in order to create or update articles. It would be interesting to be able to reuse this form for both templates. This is where template "partials" came in handy: these are template snippets that can be easily "included" into other templates to avoid duplications of code. Let's create a `src/templates/partials/article_form.html` partial with the following content: @@ -777,7 +785,7 @@ Let's create a `src/templates/partials/article_form.html` partial with the follo This partial template contains the exact same form that we used in the creation and update templates. -Let's now make use of this partial in the `src/templates/article_create.html` and `src/templates/article_update.html` templates: +Let's now make use of this partial in the `src/templates/article_create.html` and `src/templates/article_update.html` templates by leveraging the [`include`](../templates/reference/tags.md#include) template tag: ```html title="src/templates/article_create.html" {% extend "base.html" %} @@ -799,13 +807,17 @@ Let's now make use of this partial in the `src/templates/article_create.html` an As you can see, the creation and update templates are now much more simple. +:::tip +The `include` template tag provides additional options like the ability to assign variables that are specific to the included template. Please refer to the [`include` template tag reference](../templates/reference/tags.md#include) to learn more about this mechanism. +::: + ## Refactoring: using generic handlers The handlers we implemented previously map to common web development use cases: retrieving data from the database - from a specific URL paramater - and displaying it, listing multiple objects, creating or updating records, etc. These use cases are so frequently encountered that Marten provides a set of "generic handlers" that allow to easily implement them. These generic handlers take care of these common patterns so that developers don't end up reimplementing the wheel. We could definitely leverage these generic handlers as part of our weblog application. -In this light, let's start with the `HomeHandler` class we implemented earlier: this handler essentially retrieves all the `Article` records and makes this list available to the `home.html` template. This pattern is enabled by the [`Marten::Handlers::RecordList`](pathname:///api/0.2/Marten/Handlers/RecordList.html) generic handler. In order to use it, let's modify the `src/handlers/home_handler.cr` file as follows: +In this light, let's start with the `HomeHandler` class we implemented earlier: this handler essentially retrieves all the `Article` records and makes this list available to the `home.html` template. This pattern is enabled by the [`Marten::Handlers::RecordList`](pathname:///api/0.5/Marten/Handlers/RecordList.html) generic handler. In order to use it, let's modify the `src/handlers/home_handler.cr` file as follows: ```crystal title="src/handlers/home_handler.cr" class HomeHandler < Marten::Handlers::RecordList @@ -817,7 +829,7 @@ end In the above snippet we use a few class methods in order to define how the handler should behave: `#model` allows to define the model class that should be used to retrieve the record, `#template_name` allows to define the name of the template to render, and `#list_context_name` allows to define the name of the record list variable in the template context. -Let's continue with the `ArticleDetailHandler` class: this handler retrieves a specific `Article` record from a `pk` route parameter, and "renders" it using a specific template. This pattern is enabled by the [`Marten::Handlers::RecordDetail`](pathname:///api/0.2/Marten/Handlers/RecordDetail.html) generic handler. In order to use it, let's modify the `src/handlers/article_detail_handler.cr` file as follows: +Let's continue with the `ArticleDetailHandler` class: this handler retrieves a specific `Article` record from a `pk` route parameter, and "renders" it using a specific template. This pattern is enabled by the [`Marten::Handlers::RecordDetail`](pathname:///api/0.5/Marten/Handlers/RecordDetail.html) generic handler. In order to use it, let's modify the `src/handlers/article_detail_handler.cr` file as follows: ```crystal title="src/handlers/article_detail_handler.cr" class ArticleDetailHandler < Marten::Handlers::RecordDetail @@ -829,7 +841,7 @@ end In order to configure how the handler should behave, we make use of a few class methods here as well: `#model` allows to define the model class of the record that should be retrieved, `#template_name` defines the template to render, and `#record_context_name` defines the name of the record variable in the template context. -Now let's look at the `ArticleCreateHandler` class: this class displays a form when processing GET requests, and it validates a schema that is used to create a specific record when processing POST requests. This exact pattern is enabled by the [`Marten::Handlers::RecordCreate`](pathname:///api/0.2/Marten/Handlers/RecordCreate.html) generic handler. In order to use it, we can modify the `src/handlers/article_create_handler.cr` file as follows: +Now let's look at the `ArticleCreateHandler` class: this class displays a form when processing GET requests, and it validates a schema that is used to create a specific record when processing POST requests. This exact pattern is enabled by the [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) generic handler. In order to use it, we can modify the `src/handlers/article_create_handler.cr` file as follows: ```crystal title="src/handlers/article_create_handler.cr" class ArticleCreateHandler < Marten::Handlers::RecordCreate @@ -842,7 +854,7 @@ end Here, `#model` allows to define the model class to use to create the new record, `#schema` is the schema class that should be used to validated the incoming data, `#template_name` defines the name of the template to render, and `#success_route_name` is the name of the route to redirect to after a successful record creation. -We can now look at the `ArticleUpdateHandler` class: this class retrieves a specific record and displays a form when processing GET requests, and it validates a schema whose data is used to update the record when processing POST requests. This pattern is enabled by the [`Marten::Handlers::RecordUpdate`](pathname:///api/0.2/Marten/Handlers/RecordUpdate.html) generic handler. Let's use it and let's modify the `src/handlers/article_update_handler.cr` file as follows: +We can now look at the `ArticleUpdateHandler` class: this class retrieves a specific record and displays a form when processing GET requests, and it validates a schema whose data is used to update the record when processing POST requests. This pattern is enabled by the [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html) generic handler. Let's use it and let's modify the `src/handlers/article_update_handler.cr` file as follows: ```crystal title="src/handlers/article_update_handler.cr" class ArticleUpdateHandler < Marten::Handlers::RecordUpdate @@ -856,7 +868,7 @@ end Here, `#model` allows to define the model class to use to retrieve and update the record, `#schema` is the schema class that should be used to validated the incoming data, `#template_name` defines the name of the template to render, `#success_route_name` is the name of the route to redirect to after a successful record update, and `#record_context_name` is the name of the record variable in the template context. -Finally, let's look at the `ArticleDeleteHandler` class: this handler renders a template when processing GET requests, and performs the deletion of the considered record when processing POST requests. This pattern is provided by the [`Marten::Handlers::RecordDelete`](pathname:///api/0.2/Marten/Handlers/RecordDelete.html) generic handler. In order to use it, let's modify the `src/handlers/article_delete_handler.cr` file as follows: +Finally, let's look at the `ArticleDeleteHandler` class: this handler renders a template when processing GET requests, and performs the deletion of the considered record when processing POST requests. This pattern is provided by the [`Marten::Handlers::RecordDelete`](pathname:///api/0.5/Marten/Handlers/RecordDelete.html) generic handler. In order to use it, let's modify the `src/handlers/article_delete_handler.cr` file as follows: ```crystal title="src/handlers/article_delete_handler.cr" class ArticleDeleteHandler < Marten::Handlers::RecordDelete diff --git a/docs/versioned_docs/version-0.2/handlers-and-http.mdx b/docs/versioned_docs/version-0.5/handlers-and-http.mdx similarity index 76% rename from docs/versioned_docs/version-0.2/handlers-and-http.mdx rename to docs/versioned_docs/version-0.5/handlers-and-http.mdx index 12a5613ca..01267a0ca 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http.mdx +++ b/docs/versioned_docs/version-0.5/handlers-and-http.mdx @@ -21,17 +21,26 @@ Handlers are classes that process a web request and return a response. They impl
-
- +
+ +
+
+ +
+
+
- +
## How-to's
+
+ +
diff --git a/docs/versioned_docs/version-0.5/handlers-and-http/callbacks.md b/docs/versioned_docs/version-0.5/handlers-and-http/callbacks.md new file mode 100644 index 000000000..c2688ad33 --- /dev/null +++ b/docs/versioned_docs/version-0.5/handlers-and-http/callbacks.md @@ -0,0 +1,173 @@ +--- +title: Handler callbacks +description: Learn how to define handler callbacks. +sidebar_label: Callbacks +--- + +Callbacks enable you to define logic that is triggered at different stages of a handler's lifecycle. This feature allows you to intercept incoming requests and potentially bypass the standard `#dispatch` method. This document covers the available callbacks and introduces you to the associated API, which you can use to define hooks in your handlers. + +## Overview + +As stated above, callbacks are methods that will be called when specific events occur for a specific handler instance. They need to be registered explicitly in your handler classes. There are many types of of callbacks: some are [shared between all types of handlers](#shared-handler-callbacks) while some others are specific to some kinds of generic handlers. For most types of callbacks, it is generally possible to register "before" or "after" callbacks. + +Registering a callback is as simple as calling the right callback macro (eg. `#before_dispatch`) with a symbol of the name of the method to call when the callback is executed. + +For example, the following handler leverages the [`#before_dispatch`](#before_dispatch) callback in order to redirect the user to a login page if they are not already authenticated: + +```crystal +class MyHandler < Marten::Handler + before_dispatch :require_authenticated_user + + def get + respond "Hello, authenticated user!" + end + + private def require_authenticated_user + redirect(login_url) unless user_authenticated?(request) + end +end +``` + +## Shared handler callbacks + +The following callbacks are shared between all types of handlers. + +### `before_dispatch` + +`before_dispatch` callbacks are executed _before_ a request is processed as part of the handler's `#dispatch` method. For example, this capability can be leveraged to inspect the incoming request and verify that a user is logged in: + +```crystal +class MyHandler < Marten::Handler + before_dispatch :require_authenticated_user + + def get + respond "Hello, authenticated user!" + end + + private def require_authenticated_user + redirect(login_url) unless user_authenticated?(request) + end +end +``` + +When one of the defined `before_dispatch` callbacks returns a [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object (like this is the case in the above example), this response is always used instead of calling the handler's `#dispatch` method (the latest is thus completely bypassed). + +### `after_dispatch` + +`after_dispatch` callbacks are executed _after_ a request is processed as part of the handler's `#dispatch` method. For example, such a callback can be leveraged to automatically add headers or cookies to the returned response. + +```crystal +class MyHandler < Marten::Handler + after_dispatch :add_required_header + + def get + respond "Hello, authenticated user!" + end + + private def add_required_header : Nil + response!.headers["X-Foo"] = "Bar" + end +end +``` + +Similarly to `#before_dispatch` callbacks, `#after_dispatch` callbacks can return a brand new [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object. When this is the case, this response is always used instead of the one that was returned by the handler's `#dispatch` method. + +### `before_render` + +`before_render` callbacks are invoked prior to rendering a template when generating a response that incorporates its content. This means that these callbacks are executed as part of the [`#render`](./introduction.md#render) helper method and when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](./generic-handlers.md#rendering-a-template) generic handler. + +Typically, these callbacks are used to add new variables to the [global template context](./introduction.md#global-template-context), in order to make them accessible to the template runtime. For example: + +```crystal +class MyHandler < Marten::Handlers::Template + template_name "app/my_template.html" + before_render :add_variable_to_context + + private def add_variable_to_context : Nil + context["foo"] = "bar" + end +end +``` + +Note that `before_render` callbacks can technically be used to return a [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object. When this situation arises, this response always takes precedence over the one that would've been returned following the rendering of the template. + +## Schema handler callbacks + +The following callbacks are only available for handlers that inherit from the [schema handler](./reference/generic-handlers.md#processing-a-schema). That is, handlers that inherit from [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html), but also handlers that inherit from [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) and [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html). + +These callbacks let you define logics that are triggered before or after the validation of the schema. This allows you to easily intercept validation and handle the response independently of the schema validity. All these callbacks can optionally return a [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object. When an HTTP response is returned, +all following callbacks are skipped and the obtained response is returned directly, thus bypassing responses that might have been returned after by the handler. + +### `before_schema_validation` + +`before_schema_validation` callbacks are executed _before_ a schema is checked for validity. For example, this capability can be leveraged to set an attribute on the schema object before the schema validity is checked: + +```crystal +class ArticleCreateHandler < Marten::Handlers::Schema + success_url "https://example.com/articles/list" + template_name "articles/create.html" + schema ArticleSchema + + before_schema_validation :prepare_schema + + private def prepare_schema + schema.user = request.user + end +end +``` + +### `after_schema_validation` + +`after_schema_validation` callbacks are executed right _after_ a schema is checked for validity. For example, this capability can be leveraged to call a custom method on the schema instance: + +```crystal +class ArticleCreateHandler < Marten::Handlers::Schema + success_url "https://example.com/articles/list" + template_name "articles/create.html" + schema ArticleSchema + + after_schema_validation :run_schema_post_validation + + private def run_schema_post_validation : Nil + schema.trigger_post_something + end +end +``` + +### `after_successful_schema_validation` + +`after_successful_schema_validation` callbacks are executed right _after_ a schema is checked for validity (and after possible [`after_schema_validation`](#after_schema_validation) callbacks), and only if the schema validation was successful. +For example, this capability can be leveraged to create a flash message: + +```crystal +class ArticleCreateHandler < Marten::Handlers::Schema + success_url "https://example.com/articles/list" + template_name "articles/create.html" + schema ArticleSchema + + after_successful_schema_validation :generate_success_flash_message + + private def generate_success_flash_message : Nil + flash[:notice] = "Article successfully created!" + end +end +``` + +### `after_failed_schema_validation` + +`after_failed_schema_validation` callbacks are executed right _after_ a schema is checked for validity (and after possible +[`after_schema_validation`](#after_schema_validation) callbacks), but only if the schema validation failed. For example, this capability can be leveraged to create a flash message: + +```crystal +class ArticleCreateHandler < Marten::Handlers::Schema + success_url "https://example.com/articles/list" + template_name "articles/create.html" + schema ArticleSchema + + after_failed_schema_validation :generate_failure_flash_message + + private def generate_failure_flash_message : Nil + flash[:notice] = "Article creation failed!" + end +end +``` diff --git a/docs/versioned_docs/version-0.5/handlers-and-http/cookies.md b/docs/versioned_docs/version-0.5/handlers-and-http/cookies.md new file mode 100644 index 000000000..d0c4c58b9 --- /dev/null +++ b/docs/versioned_docs/version-0.5/handlers-and-http/cookies.md @@ -0,0 +1,150 @@ +--- +title: Cookies +description: Learn how to use cookies to persist data on the client. +--- + +Handlers are able to interact with a cookies store that you can use to store small amounts of data - called cookies - on the client. This data will be persisted across requests and will be made accessible with every incoming request. + +## Basic usage + +### Accessing the cookie store + +Cookies can be interacted with by leveraging a cookie store: an instance of [`Marten::HTTP::Cookies`](pathname:///api/0.5/Marten/HTTP/Cookies.html) that provides a hash-like interface allowing to retrieve and store cookie values. This cookie store can be accessed from three different places: + +* Handlers can access it through the use of the [`#cookies`](pathname:///api/0.5/Marten/Handlers/Cookies.html#cookies(*args%2C**options)-instance-method) method. +* [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html) objects give access to the cookies associated with the request via the [`#cookies`](pathname:///api/0.5/Marten/HTTP/Request.html#cookies-instance-method) method. +* [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) objects give access to the cookies that will be returned with the HTTP response via the [`#cookies`](pathname:///api/0.5/Marten/HTTP/Response.html#cookies%3AMarten%3A%3AHTTP%3A%3ACookies-instance-method) method. + + +Here is a very simple example of how to interact with the cookies store within a handler: + +```crystal +class MyHandler < Marten::Handler + def get + cookies[:foo] = "bar" + respond "Hello World!" + end +end +``` + +### Retrieving cookie values + +The most simple way to retrieve the value of a cookie is to leverage the [`#[]`](pathname:///api/0.5/Marten/HTTP/Cookies.html#[](name%3AString|Symbol)-instance-method) method or one of its variants. + +For example, the following lines could be used to read the value of a cookie named `foo`: + +```crystal +request.cookies[:foo] # => returns the value of "foo" or raises a KeyError if not found +request.cookies[:foo]? # => returns the value of "foo" or returns nil if not found +``` + +Alternatively, the [`#fetch`](pathname:///api/0.5/Marten/HTTP/Cookies.html#fetch(name%3AString|Symbol%2Cdefault%3Dnil)-instance-method) method can also be leveraged in order to execute a block or return a default value if the specified cookie is not found: + +```crystal +request.cookies.fetch(:foo, "defaultval") +request.cookies.fetch(:foo) { "defaultval" } +``` + +### Setting cookies + +The most simple way to set a new cookie is to call the [`#[]=`](pathname:///api/0.5/Marten/HTTP/Cookies.html#[]%3D(name%2Cvalue)-instance-method) method on a cookie store. For example: + +```crystal +request.cookies[:foo] = "bar" +``` + +Calling this method will create a new cookie with the specified name and value. It should be noted that cookies created with the [`#[]=`](pathname:///api/0.5/Marten/HTTP/Cookies.html#[]%3D(name%2Cvalue)-instance-method) method will _not_ expire, will be associated with the root path (`/`), and will not be secure. + +Alternatively, it is possible to leverage the [`#set`](pathname:///api/0.5/Marten/HTTP/Cookies.html#set(name%3AString|Symbol%2Cvalue%2Cexpires%3ATime|Nil%3Dnil%2Cpath%3AString%3D"/"%2Cdomain%3AString|Nil%3Dnil%2Csecure%3ABool%3Dfalse%2Chttp_only%3ABool%3Dfalse%2Csame_site%3ANil|String|Symbol%3Dnil)%3ANil-instance-method) in order to specify custom cookie properties while setting new cookie values. For example: + +```crystal +request.cookies.set( + :foo, + "bar", + expires: 2.days.from_now, + secure: true, + same_site: "lax" +) +``` + +Appart from the cookie name and value, the [`#set`](pathname:///api/0.5/Marten/HTTP/Cookies.html#set(name%3AString|Symbol%2Cvalue%2Cexpires%3ATime|Nil%3Dnil%2Cpath%3AString%3D"/"%2Cdomain%3AString|Nil%3Dnil%2Csecure%3ABool%3Dfalse%2Chttp_only%3ABool%3Dfalse%2Csame_site%3ANil|String|Symbol%3Dnil)%3ANil-instance-method) method allows to define some additional cookie properties: + +* The cookie expiry datetime (`expires` argument). +* The cookie `path`. +* The associated `domain` (useful in order to define cross-domain cookies). +* Whether or not the cookie should be sent for HTTPS requests only (`secure` argument). +* Whether or not client-side scripts should have access to the cookie (`http_only` argument). +* The `same_site` policy (accepted values are `"lax"` or `"strict"`). + +### Deleting cookies + +Cookies can be deleted by leveraging the [`#delete`](pathname:///api/0.5/Marten/HTTP/Cookies.html#delete(name%3AString|Symbol%2Cpath%3AString%3D"/"%2Cdomain%3AString|Nil%3Dnil%2Csame_site%3ANil|String|Symbol%3Dnil)%3AString|Nil-instance-method) method. This method will delete a specific cookie and return its value, or `nil` if the cookie does not exist: + +```crystal +request.cookies.delete(:foo) +``` + +Apart from the name of the cookie, this method allows to define some additional properties of the cookie to delete: + +* The cookie `path`. +* The associated `domain` (useful in order to define cross-domain cookies). +* The `same_site` policy (accepted values are `"lax"` or `"strict"`). + +Note that the `path`, `domain`, and `same_site` values should always be the same as the ones that were used to create the cookie in the first place. Otherwise, the cookie might not be deleted properly. + +## Signed cookies + +In addition to the [regular cookie store](#accessing-the-cookie-store), Marten provides a signed cookie store version (which is accessible through the use of the [`Marten::HTTP::Cookies#signed`](pathname:///api/0.5/Marten/HTTP/Cookies.html#signed-instance-method) method) where cookies are signed (but **not** encrypted). This means that whenever a cookie is requested from this store, the signed representation of the corresponding value will be verified. This is useful to create cookies that can't be tampered by users, but it should be noted that the actual data can still be read by the client technically. + +All the methods that can be used with the regular cookie store that were highlighted in [Basic usage](#basic-usage) can also be used with the signed cookie store: + +```crystal +# Retrieving cookies... +request.signed.cookies[:foo] +request.signed.cookies[:foo]? +request.signed.cookies.fetch(:foo, "defaultval") +request.signed.cookies.fetch(:foo) { "defaultval" } + +# Setting cookies... +request.signed.cookies[:foo] = "bar" +request.signed.cookies.set(:foo, "bar", expires: 2.days.from_now) + +# Deleting cookies... +request.signed.cookies.delete(:foo) +``` + +The signed cookie store uses a [`Marten::Core::Signer`](pathname:///api/0.5/Marten/Core/Signer.html) signer object in order to sign cookie values and to verify the signature of retrieved cookies. This means that cookies are signed with HMAC signatures that use the **SHA256** hash algorithm. + +:::info +Only cookie _values_ are signed. Cookie _names_ are not signed. +::: + +## Encrypted cookies + +In addition to the [regular cookie store](#accessing-the-cookie-store), Marten provides an encrypted cookie store version (which is accessible through the use of the [`Marten::HTTP::Cookies#encrypted`](pathname:///api/0.5/Marten/HTTP/Cookies.html#encrypted-instance-method) method) where cookies are signed and encrypted. This means that whenever a cookie is requested from this store, the raw value of the cookie will be decrypted and its signature will be verified. This is useful to create cookies whose values can't be read nor tampered by users. + +All the methods that can be used with the regular cookie store that were highlighted in [Basic usage](#basic-usage) can also be used with the encrypted cookie store: + +```crystal +# Retrieving cookies... +request.encrypted.cookies[:foo] +request.encrypted.cookies[:foo]? +request.encrypted.cookies.fetch(:foo, "defaultval") +request.encrypted.cookies.fetch(:foo) { "defaultval" } + +# Setting cookies... +request.encrypted.cookies[:foo] = "bar" +request.encrypted.cookies.set(:foo, "bar", expires: 2.days.from_now) + +# Deleting cookies... +request.encrypted.cookies.delete(:foo) +``` + +The signed cookie store uses a [`Marten::Core::Encryptor`](pathname:///api/0.5/Marten/Core/Encryptor.html) encryptor object in order to encrypt and sign cookie values. This means that cookies are: + +* encrypted with an **aes-256-cbc** cipher. +* signed with HMAC signatures that use the **SHA256** hash algorithm. + +:::info +Only cookie _values_ are encrypted and signed. Cookie _names_ are not encrypted. +::: diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/error-handlers.md b/docs/versioned_docs/version-0.5/handlers-and-http/error-handlers.md similarity index 88% rename from docs/versioned_docs/version-0.2/handlers-and-http/error-handlers.md rename to docs/versioned_docs/version-0.5/handlers-and-http/error-handlers.md index 60037de0b..f4909f054 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/error-handlers.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/error-handlers.md @@ -19,10 +19,10 @@ Note that you don't need to manually interact with these default error handlers: ### Page Not Found (404) -A Page Not Found (404) response is automatically returned by the [`Marten::Handlers::Defaults::PageNotFound`](pathname:///api/0.2/Marten/Handlers/Defaults/PageNotFound.html) handler when: +A Page Not Found (404) response is automatically returned by the [`Marten::Handlers::Defaults::PageNotFound`](pathname:///api/0.5/Marten/Handlers/Defaults/PageNotFound.html) handler when: * a route cannot be found for an incoming request -* the [`Marten::HTTP::Errors::NotFound`](pathname:///api/0.2/Marten/HTTP/Errors/NotFound.html) exception is raised +* the [`Marten::HTTP::Errors::NotFound`](pathname:///api/0.5/Marten/HTTP/Errors/NotFound.html) exception is raised :::info If your project is running in debug mode, Marten will automatically show a different page containing specific information about the original request instead of using the default Page Not Found handler. @@ -30,7 +30,7 @@ If your project is running in debug mode, Marten will automatically show a diffe ### Internal Server Error (500) -An Internal Server Error (500) response is automatically returned by the [`Marten::Handlers::Defaults::ServerError`](pathname:///api/0.2/Marten/Handlers/Defaults/ServerError.html) handler when an unhandled exception is intercepted by the Marten server. +An Internal Server Error (500) response is automatically returned by the [`Marten::Handlers::Defaults::ServerError`](pathname:///api/0.5/Marten/Handlers/Defaults/ServerError.html) handler when an unhandled exception is intercepted by the Marten server. :::info If your project is running in debug mode, Marten will automatically show a different page containing specific information about the error that occurred (traceback, request details, etc) instead of using the default Internal Server Error handler. @@ -38,11 +38,11 @@ If your project is running in debug mode, Marten will automatically show a diffe ### Bad Request (400) -A Bad Request (400) response is automatically returned by the [`Marten::Handlers::Defaults::BadRequest`](pathname:///api/0.2/Marten/Handlers/Defaults/BadRequest.html) handler when the [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.2/Marten/HTTP/Errors/SuspiciousOperation.html) exception is raised. +A Bad Request (400) response is automatically returned by the [`Marten::Handlers::Defaults::BadRequest`](pathname:///api/0.5/Marten/Handlers/Defaults/BadRequest.html) handler when the [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.5/Marten/HTTP/Errors/SuspiciousOperation.html) exception is raised. ### Forbidden (403) -A Forbidden (403) response is automatically returned by the [`Marten::Handlers::Defaults::PermissionDenied`](pathname:///api/0.2/Marten/Handlers/Defaults/PermissionDenied.html) handler when the [`Marten::HTTP::Errors::PermissionDenied`](pathname:///api/0.2/Marten/HTTP/Errors/PermissionDenied.html) exception is raised. +A Forbidden (403) response is automatically returned by the [`Marten::Handlers::Defaults::PermissionDenied`](pathname:///api/0.5/Marten/Handlers/Defaults/PermissionDenied.html) handler when the [`Marten::HTTP::Errors::PermissionDenied`](pathname:///api/0.5/Marten/HTTP/Errors/PermissionDenied.html) exception is raised. ## Customizing error handlers diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/generic-handlers.md b/docs/versioned_docs/version-0.5/handlers-and-http/generic-handlers.md similarity index 84% rename from docs/versioned_docs/version-0.2/handlers-and-http/generic-handlers.md rename to docs/versioned_docs/version-0.5/handlers-and-http/generic-handlers.md index f4a1add79..17dec0d4a 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/generic-handlers.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/generic-handlers.md @@ -23,7 +23,7 @@ Finally, it should be noted that using generic handlers is totally optional. The ### Performing a redirect -Having a handler that performs a redirect can be easily achieved by subclassing the [`Marten::Handlers::Redirect`](pathname:///api/0.2/Marten/Handlers/Redirect.html) generic handler. For example, you could easily define a handler that redirects to a `articles:list` route with the following snippet: +Having a handler that performs a redirect can be easily achieved by subclassing the [`Marten::Handlers::Redirect`](pathname:///api/0.5/Marten/Handlers/Redirect.html) generic handler. For example, you could easily define a handler that redirects to a `articles:list` route with the following snippet: ```crystal class ArticlesRedirectHandler < Marten::Handlers::Redirect @@ -59,7 +59,7 @@ end ### Rendering a template -One of the most frequent things you will want to do when writing handlers is to return HTML responses containing rendered [templates](../templates.mdx). To do so, you can obviously define a regular handler and make use of the [`#render`](./introduction.md#render) helper. But, you may also want to leverage the [`Marten::Handlers::Template`](pathname:///api/0.2/Marten/Handlers/Template.html) generic handler. +One of the most frequent things you will want to do when writing handlers is to return HTML responses containing rendered [templates](../templates.mdx). To do so, you can obviously define a regular handler and make use of the [`#render`](./introduction.md#render) helper. But, you may also want to leverage the [`Marten::Handlers::Template`](pathname:///api/0.5/Marten/Handlers/Template.html) generic handler. This generic handler will return a 200 OK HTTP response containing a rendered HTML template. To make use of it, you can simply define a subclass of it and ensure that you call the `#template_name` class method in order to define the template that will be rendered: @@ -69,21 +69,25 @@ class HomeHandler < Marten::Handlers::Template end ``` -If you need to, it is possible to customize the context that is used to render the configured template. To do so, you can define a `#context` method that returns a hash or a named tuple with the values you want to make available to your template: +If you need to, it is possible to customize the context that is used to render the configured template. To do so, you can define a [`before_render`](./callbacks.md#before_render) callback and add new variables to the [global template context](./introduction.md#global-template-context) (which functions similarly to a hash object): ```crystal class HomeHandler < Marten::Handlers::Template template_name "app/home.html" - def context - { "recent_articles" => Article.all.order("-published_at")[:5] } + before_render add_recent_articles_to_context + + private def add_recent_articles_to_context : Nil + context[:recent_articles] = Article.all.order("-published_at")[:5] end end ``` +Variables that are added to the global template context will automatically be available to the configured template's runtime. + ### Displaying a model record -It is possible to render a template that showcases a specific model record by leveraging the [`Marten::Handlers::RecordDetail`](pathname:///api/0.2/Marten/Handlers/RecordDetail.html) generic handler. +It is possible to render a template that showcases a specific model record by leveraging the [`Marten::Handlers::RecordDetail`](pathname:///api/0.5/Marten/Handlers/RecordDetail.html) generic handler. For example, it would be possible to render an `articles/detail.html` template showcasing a specific `Article` model record with the following handler: @@ -107,12 +111,12 @@ For example, the template associated with this handler could be something like t ### Processing a form -It is possible to use the [`Marten::Handlers::Schema`](pathname:///api/0.2/Marten/Handlers/Schema.html) generic handler in order to process form data with a [schema](../schemas.mdx). +It is possible to use the [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) generic handler in order to process form data with a [schema](../schemas.mdx). To do so, it is necessary: -* to specify the schema class to use to validate the incoming POST data through the use of the `#schema` class method -* to specify the template to render by using the `#template_name` class method: this template will likely generate an HTML form +* to specify the schema class to use to validate the incoming POST data through the use of the `#schema` macro +* to specify the template to render by using the `#template_name` class method: this template will likely generate an HTML form * to specify the route to redirect to when the schema is valid via the `#success_route_name` class method For example: @@ -138,4 +142,8 @@ end By default, such a handler will render the configured template when the incoming request is a GET or for POST requests if the data cannot be validated using the specified schema (in that case, the template is expected to use the invalid schema to display a form with the right errored inputs). The specified template can have access to the configured schema through the use of the `schema` object in the template context. -If the schema is valid, a temporary redirect is issued by using the URL corresponding to the `#success_route_name` value (although it should be noted that the way to generate this success URL can be overridden by defining a `#success_url` method). By default, the handler does nothing when the processed schema is valid (except redirecting to the success URL). That's why it can be helpful to override the `#process_valid_schema` method to implement any logic that should be triggered after a successful schema validation. +If the schema is valid, a temporary redirect is issued by using the URL corresponding to the `#success_route_name` value (although it should be noted that the way to generate this success URL can be overridden by defining a `#success_url` method). By default, the handler does nothing when the processed schema is valid (except redirecting to the success URL). + +:::tip +Handlers making use of the [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) generic handler can leverage additional types of callbacks. Please head over to [Schema handler callbacks](./callbacks.md#schema-handler-callbacks) to learn more about those. +::: diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/how-to/create-custom-route-parameters.md b/docs/versioned_docs/version-0.5/handlers-and-http/how-to/create-custom-route-parameters.md similarity index 76% rename from docs/versioned_docs/version-0.2/handlers-and-http/how-to/create-custom-route-parameters.md rename to docs/versioned_docs/version-0.5/handlers-and-http/how-to/create-custom-route-parameters.md index 0a8538974..ecc0ae807 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/how-to/create-custom-route-parameters.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/how-to/create-custom-route-parameters.md @@ -7,21 +7,25 @@ Although Marten has built-in support for [common route parameters](../routing.md ## Defining a route parameter -In order to implement custom parameters, you need to subclass the [`Marten::Routing::Parameter::Base`](pathname:///api/0.2/Marten/Routing/Parameter/Base.html) abstract class. Each parameter class is responsible for: +In order to implement custom parameters, you need to subclass the [`Marten::Routing::Parameter::Base`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html) abstract class. Each parameter class is responsible for: -* defining a [regex](https://crystal-lang.org/reference/master/syntax_and_semantics/literals/regex.html) allowing to match the parameters in raw paths (which can be done through the use of the [`#regex`](pathname:///api/0.2/Marten/Routing/Parameter/Base.html#regex(regex)-macro) macro) -* defining _how_ the route parameter value should be deserialized (which can be done by implementing a [`#loads`](pathname:///api/0.2/Marten/Routing/Parameter/Base.html#loads(value%3A%3A%3AString)-instance-method) method) -* defining _how_ the route parameter value should serialized (which can be done by implementing a [`#dumps`](pathname:///api/0.2/Marten/Routing/Parameter/Base.html#dumps(value)%3A%3A%3AString%3F-instance-method) method) +* defining a Regex allowing to match the parameters in raw paths (which can be done by implementing a [`#regex`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html#regex%3ARegex-instance-method) method) +* defining _how_ the route parameter value should be deserialized (which can be done by implementing a [`#loads`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html#loads(value%3A%3A%3AString)-instance-method) method) +* defining _how_ the route parameter value should serialized (which can be done by implementing a [`#dumps`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html#dumps(value)%3A%3A%3AString%3F-instance-method) method) -The [`#loads`](pathname:///api/0.2/Marten/Routing/Parameter/Base.html#loads(value%3A%3A%3AString)-instance-method) method takes the raw parameter (string) as argument and is expected to return the final Crystal object corresponding to the route parameter (this is the object that will be forwarded to the handler in the route parameters hash). +The [`#regex`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html#regex%3ARegex-instance-method) method takes no arguments and must return a valid [`Regex`](https://crystal-lang.org/api/Regex.html) object. -The [`#dumps`](pathname:///api/0.2/Marten/Routing/Parameter/Base.html#dumps(value)%3A%3A%3AString%3F-instance-method) method takes the final route parameter object as argument and must return the corresponding string representation. Note that this method can either return a string or `nil`: `nil` means that the passed value couldn't be serialized properly, which will make any URL reverse resolution fail with a `Marten::Routing::Errors::NoReverseMatch` error. +The [`#loads`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html#loads(value%3A%3A%3AString)-instance-method) method takes the raw parameter (string) as argument and is expected to return the final Crystal object corresponding to the route parameter (this is the object that will be forwarded to the handler in the route parameters hash). + +The [`#dumps`](pathname:///api/0.5/Marten/Routing/Parameter/Base.html#dumps(value)%3A%3A%3AString%3F-instance-method) method takes the final route parameter object as argument and must return the corresponding string representation. Note that this method can either return a string or `nil`: `nil` means that the passed value couldn't be serialized properly, which will make any URL reverse resolution fail with a `Marten::Routing::Errors::NoReverseMatch` error. For example, a "year" (1000-2999) route parameter could be implemented as follows: ```crystal class YearParameter < Marten::Routing::Parameter::Base - regex /[12][0-9]{3}/ + def regex : Regex + /[12][0-9]{3}/ + end def loads(value : ::String) : UInt64 value.to_u64 @@ -43,7 +47,7 @@ end In order to be able to use custom route parameters in your [route definitions](../routing.md#specifying-route-parameters), you must register them to Marten's global routing parameters registry. -To do so, you will have to call the [`Marten::Routing::Parameter#register`](pathname:///api/0.2/Marten/Routing/Parameter.html#register(id%3A%3A%3AString|Symbol%2Cparameter_klass%3ABase.class)-class-method) method with the identifier of the parameter you wish to use in route path definitions, and the actual parameter class. For example: +To do so, you will have to call the [`Marten::Routing::Parameter#register`](pathname:///api/0.5/Marten/Routing/Parameter.html#register(id%3A%3A%3AString|Symbol%2Cparameter_klass%3ABase.class)-class-method) method with the identifier of the parameter you wish to use in route path definitions, and the actual parameter class. For example: ```crystal Marten::Routing::Parameter.register(:year, YearParameter) diff --git a/docs/versioned_docs/version-0.5/handlers-and-http/how-to/customize-handler-template-contexts.md b/docs/versioned_docs/version-0.5/handlers-and-http/how-to/customize-handler-template-contexts.md new file mode 100644 index 000000000..1d3f0864d --- /dev/null +++ b/docs/versioned_docs/version-0.5/handlers-and-http/how-to/customize-handler-template-contexts.md @@ -0,0 +1,98 @@ +--- +title: Customize handler template contexts +sidebar_label: Customize handler contexts +description: How to customize handler template contexts. +--- + +This guide covers how to easily customize the [template context](../../templates/introduction.md) of handlers involving template renderings and how to add variables to it. Doing so will allow you to leverage custom variables in your handler templates and easily manipulate the appearance and content of your web pages, tailoring them to suit your specific needs and preferences. + +## Requirements + +It is possible to customize the template context used by most handlers involving template renderings. This is the case for: + +* Handlers that make use of the [`#render`](../introduction.md#render) helper method. +* Handlers that inherit from the [`Marten::Handlers::Template`](../reference/generic-handlers.md#rendering-a-template), [`Marten::Handlers::Schema`](../reference/generic-handlers.md#processing-a-schema), [`Marten::Handlers::RecordCreate`](../reference/generic-handlers.md#creating-a-record), [`Marten::Handlers::RecordDetail`](../reference/generic-handlers.md#displaying-a-record), [`Marten::Handlers::RecordUpdate`](../reference/generic-handlers.md#updating-a-record), or [`Marten::Handlers::RecordDelete`](../reference/generic-handlers.md#deleting-a-record) generic handlers. + +## Customizing the template context of a handler + +If your handler adheres to the [requirements](#requirements) mentioned above, then the simplest way to customize what variables are made available to the considered template context is to leverage the [`#before_render`](../callbacks.md#before_render) callback. + +This callback is invoked prior to rendering a template when generating a response that incorporates its content. This means that they can be used to add new variables to the [global handler template context](../introduction.md#global-template-context) so that they become accessible to the template runtime. + +For example: + +```crystal +class MyHandler < Marten::Handlers::Template + template_name "app/my_template.html" + before_render :add_foo_to_context + + private def add_foo_to_context : Nil + context["foo"] = "bar" + end +end +``` + +In the above snippet, a very simple handler (that inherits from the [`Marten::Handlers::Template`](../reference/generic-handlers.md#rendering-a-template) generic handler) defines a [`#before_render`](../callbacks.md#before_render) callback in which a `foo` variable is added to the template context. + +## A concrete example: currently active link in navigation sections + +To exemplify this functionality, let's consider a straightforward scenario involving navigation sections. Typically, it's essential to determine the currently active item within the navigation. This can be effortlessly accomplished by utilizing a dedicated template variable. + +For example, let's assume that our navigation template looks something like this: + +```html + +``` + +This template utilizes a `nav_bar_item` variable to determine the currently active navigation bar item and applies a dedicated `active` CSS class to the corresponding link based on the value of this variable. + +In order for this navigation to work as intended, we need to ensure that the applicable handlers define the `nav_bar_item` template variables. We could utilize the method described previously and simply define a [`#before_render`](../callbacks.md#before_render) callback in the handlers that need to define this template variable. A better solution though would be to define a concern module that eases the process of doing this. + +For example, we could define a `NavBarActiveable` module that automatically sets the `nav_bar_item` template variable based on the value of a class variable that is set by handler classes making use of it: + +```crystal +module NavBarActiveable + macro included + class_getter nav_bar_item : String? + + extend NavBarActiveable::ClassMethods + + before_render :add_nav_bar_item_to_context + end + + module ClassMethods + def nav_bar_item(item : String | Symbol) + @@nav_bar_item = item.to_s + end + end + + private def add_nav_bar_item_to_context + context[:nav_bar_item] = self.class.nav_bar_item + end +end +``` + +Using this approach, defining the `nav_bar_item` variable in a handler would be as simple as including the `NavBarActiveable` module in the handler class and calling the `#nav_bar_item` method: + +```crystal +class MyHandler < Marten::Handlers::Template + include NavBarActiveable + + template_name "app/my_template.html" + nav_bar_item :home +end +``` diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/introduction.md b/docs/versioned_docs/version-0.5/handlers-and-http/introduction.md similarity index 64% rename from docs/versioned_docs/version-0.2/handlers-and-http/introduction.md rename to docs/versioned_docs/version-0.5/handlers-and-http/introduction.md index b7e4427b2..fa4d1a49b 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/introduction.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/introduction.md @@ -8,7 +8,7 @@ Handlers are classes whose responsibility is to process web requests and to retu ## Writing handlers -At their core, handlers are subclasses of the [`Marten::Handler`](pathname:///api/0.2/Marten/Handlers/Base.html) class. These classes are usually defined under a `handlers` folder, at the root of a Marten project or application. Here is an example of a very simple handler: +At their core, handlers are subclasses of the [`Marten::Handler`](pathname:///api/0.5/Marten/Handlers/Base.html) class. These classes are usually defined under a `handlers` folder, at the root of a Marten project or application. Here is an example of a very simple handler: ```crystal class SimpleHandler < Marten::Handler @@ -20,7 +20,7 @@ end The above handler returns a `200 OK` response containing a short text, regardless of the incoming HTTP request method. -Handlers are initialized from a [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html) object and an optional set of routing parameters. Their inner logic is executed when calling the `#dispatch` method, which _must_ return a [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) object. +Handlers are initialized from a [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html) object and an optional set of routing parameters. Their inner logic is executed when calling the `#dispatch` method, which _must_ return a [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object. When the `#dispatch` method is explicitly overridden, it is responsible for applying different logics in order to handle the various incoming HTTP request methods. For example, a handler might display an HTML page containing a form when handling a `GET` request, and it might process possible form data when handling a `POST` request: @@ -56,30 +56,30 @@ If a handler's logic is defined like in the above example, trying to access such ### The `request` and `response` objects -As mentioned previously, a handler is always initialized from an incoming HTTP request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html)) and is required to return an HTTP response object (instance of [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html)) as part of its `#dispatch` method. +As mentioned previously, a handler is always initialized from an incoming HTTP request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html)) and is required to return an HTTP response object (instance of [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html)) as part of its `#dispatch` method. The `request` object gives access to a set of useful information and attributes associated with the incoming request. Things like the HTTP request verb, headers, or query parameters can be accessed through this object. The most common methods that you can use are listed below: | Method | Description | | ----------- | ----------- | | `#body` | Returns the raw body of the request as a string. | -| `#cookies` | Returns a hash-like object (instance of [`Marten::HTTP::Cookies`](pathname:///api/0.2/Marten/HTTP/Cookies.html)) containing the cookies associated with the request. | -| `#data` | Returns a hash-like object (instance of [`Marten::HTTP::Params::Data`](pathname:///api/0.2/Marten/HTTP/Params/Data.html)) containing the request data. | -| `#flash` | Returns a hash-like object (instance of [`Marten::HTTP::FlashStore`](pathname:///api/0.2/Marten/HTTP/FlashStore.html)) containing the flash messages available to the current request. | -| `#headers` | Returns a hash-like object (instance of [`Marten::HTTP::Headers`](pathname:///api/0.2/Marten/HTTP/Headers.html)) containing the headers embedded in the request. | +| `#cookies` | Returns a hash-like object (instance of [`Marten::HTTP::Cookies`](pathname:///api/0.5/Marten/HTTP/Cookies.html)) containing the cookies associated with the request. | +| `#data` | Returns a hash-like object (instance of [`Marten::HTTP::Params::Data`](pathname:///api/0.5/Marten/HTTP/Params/Data.html)) containing the request data. | +| `#flash` | Returns a hash-like object (instance of [`Marten::HTTP::FlashStore`](pathname:///api/0.5/Marten/HTTP/FlashStore.html)) containing the flash messages available to the current request. | +| `#headers` | Returns a hash-like object (instance of [`Marten::HTTP::Headers`](pathname:///api/0.5/Marten/HTTP/Headers.html)) containing the headers embedded in the request. | | `#host` | Returns the host associated with the considered request. | | `#method` | Returns the considered HTTP request method (`GET`, `POST`, `PUT`, etc). | -| `#query_params` | Returns a hash-like object (instance of [`Marten::HTTP::Params::Query`](pathname:///api/0.2/Marten/HTTP/Params/Query.html)) containing the HTTP GET parameters embedded in the request. | -| `#session` | Returns a hash-like object (instance of [`Marten::HTTP::Session::Store::Base`](pathname:///api/0.2/Marten/HTTP/Session/Store/Base.html)) corresponding to the session store for the current request. | +| `#query_params` | Returns a hash-like object (instance of [`Marten::HTTP::Params::Query`](pathname:///api/0.5/Marten/HTTP/Params/Query.html)) containing the HTTP GET parameters embedded in the request. | +| `#session` | Returns a hash-like object (instance of [`Marten::HTTP::Session::Store::Base`](pathname:///api/0.5/Marten/HTTP/Session/Store/Base.html)) corresponding to the session store for the current request. | -The `response` object corresponds to the HTTP response that is returned to the client. Response objects can be created by initializing the [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) class directly (or one of its subclasses) or by using [response helper methods](#response-helper-methods). Once initialized, these objects can be mutated to further configure what is sent back to the browser. The most common methods that you can use in this regard are listed below: +The `response` object corresponds to the HTTP response that is returned to the client. Response objects can be created by initializing the [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) class directly (or one of its subclasses) or by using [response helper methods](#response-helper-methods). Once initialized, these objects can be mutated to further configure what is sent back to the browser. The most common methods that you can use in this regard are listed below: | Method | Description | | ----------- | ----------- | | `#content` | Returns the content of the response as a string. | | `#content_type` | Returns the content type of the response as a string. | -| `#cookies` | Returns a hash-like object (instance of [`Marten::HTTP::Cookies`](pathname:///api/0.2/Marten/HTTP/Cookies.html)) containing the cookies that will be sent with the response. | -| `#headers` | Returns a hash-like object (instance of [`Marten::HTTP::Headers`](pathname:///api/0.2/Marten/HTTP/Headers.html)) containg the headers that will be used for the response. | +| `#cookies` | Returns a hash-like object (instance of [`Marten::HTTP::Cookies`](pathname:///api/0.5/Marten/HTTP/Cookies.html)) containing the cookies that will be sent with the response. | +| `#headers` | Returns a hash-like object (instance of [`Marten::HTTP::Headers`](pathname:///api/0.5/Marten/HTTP/Headers.html)) containg the headers that will be used for the response. | | `#status` | Returns the status of the response (eg. 200 or 404). | ### Parameters @@ -100,9 +100,13 @@ class FormHandler < Marten::Handler end ``` +:::tip +Note that you can use either strings or symbols when interacting with the routing parameters returned by the `#params` method. +::: + ### Response helper methods -Technically, it is possible to forge HTTP responses by instantiating the [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) class directly (or one of its subclasses such as [`Marten::HTTP::Response::Found`](pathname:///api/0.2/Marten/HTTP/Response/Found.html) for example). That being said, Marten provides a set of helper methods that can be used to conveniently forge responses for various use cases: +Technically, it is possible to forge HTTP responses by instantiating the [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) class directly (or one of its subclasses such as [`Marten::HTTP::Response::Found`](pathname:///api/0.5/Marten/HTTP/Response/Found.html) for example). That being said, Marten provides a set of helper methods that can be used to conveniently forge responses for various use cases: #### `respond` @@ -114,6 +118,14 @@ respond("Response content", content_type: "text/html", status: 200) Unless specified, the `content_type` is set to `text/html` and the `status` is set to `200`. +:::tip +You can also express the `status` of the response as a symbol that must comply with the values of the [`HTTP::Status`](https://crystal-lang.org/api/HTTP/Status.html) enum. For example: + +```crystal +respond("Response content", content_type: "text/html", status: :ok) +``` +::: + #### `render` `render` allows returning an HTTP response whose content is generated by rendering a specific [template](../templates.mdx). The template can be rendered by specifying a context hash or named tuple. For example: @@ -124,6 +136,14 @@ render("path/to/template.html", context: { foo: "bar" }, content_type: "text/htm Unless specified, the `content_type` is set to `text/html` and the `status` is set to `200`. +:::tip +You can also express the `status` of the response as a symbol that must comply with the values of the [`HTTP::Status`](https://crystal-lang.org/api/HTTP/Status.html) enum. For example: + +```crystal +render("path/to/template.html", context: { foo: "bar" }, content_type: "text/html", status: :ok) +``` +::: + #### `redirect` `#redirect` allows forging a redirect HTTP response. It requires a `url` and accepts an optional `permanent` argument in order to define whether a permanent redirect is returned (301 Moved Permanently) or a temporary one (302 Found): @@ -142,6 +162,14 @@ Unless explicitly specified, `permanent` will automatically be set to `false`. head(404) ``` +:::tip +You can also express the `status` of the response as a symbol that must comply with the values of the [`HTTP::Status`](https://crystal-lang.org/api/HTTP/Status.html) enum. For example: + +```crystal +head :not_found +``` +::: + #### `json` `json` allows forging an HTTP response with the `application/json` content type. It can be used with a raw JSON string, or any serializable object: @@ -152,50 +180,43 @@ json({ foo: "bar" }, status: 200) Unless specified, the `status` is set to `200`. -### Callbacks +:::tip +You can also express the `status` of the response as a symbol that must comply with the values of the [`HTTP::Status`](https://crystal-lang.org/api/HTTP/Status.html) enum. For example: -Callbacks let you define logics that are triggered before or after a handler's dispatch flow. This allows you to easily intercept the incoming request and completely bypass the execution of the regular `#dispatch` method for example. Two callbacks are supported: `before_dispatch` and `after_dispatch`. +```crystal +json({ foo: "bar" }, status: :ok) +``` +::: -#### `before_dispatch` +### Callbacks -`before_dispatch` callbacks are executed _before_ a request is processed as part of the handler's `#dispatch` method. For example, this capability can be leveraged to inspect the incoming request and verify that a user is logged in: +It is possible to define callbacks in order to bind methods and logics to specific events in the lifecycle of your handlers. For example, it is possible to define callbacks that run before a handler's `#dispatch` method gets executed, or after it! -```crystal -class MyHandler < Marten::Handler - before_dispatch :require_authenticated_user +Please head over to the [Handler callbacks](./callbacks.md) guide in order to learn more about handler callbacks. - def get - respond "Hello, authenticated user!" - end +### Generic handlers - private def require_authenticated_user - redirect(login_url) unless user_authenticated?(request) - end -end -``` +Marten provides a set of generic handlers that can be used to perform common application tasks such as displaying lists of records, deleting entries, or rendering [templates](../templates/introduction.md). This saves developers from reinventing common patterns. -When one of the defined `before_dispatch` callbacks returns a [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) object, this response is always used instead of calling the handler's `#dispatch` method (the latest is thus completely bypassed). +Please head over to the [Generic handlers](./generic-handlers.md) guide in order to learn more about available generic handlers. -#### `after_dispatch` +### Global template context -`after_dispatch` callbacks are executed _after_ a request is processed as part of the handler's `#dispatch` method. For example, such a callback can be leveraged to automatically add headers or cookies to the returned response. +All handlers have access to a [`#context`](pathname:///api/0.5/Marten/Handlers/Base.html#context-instance-method) method that returns a [template](../templates/introduction.md) context object. This "global" context object is available for the lifetime of the considered handler and can be mutated in order to define which variables are made available to the template runtime when rendering templates through the use of the [`#render`](#render) helper method or when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](./generic-handlers.md#rendering-a-template) generic handler. -```crystal -class MyHandler < Marten::Handler - after_dispatch :add_required_header +To modify this context object effectively, it's recommended to utilize [`before_render`](./callbacks.md#before_render) callbacks, which are invoked just before rendering a template within a handler. For example, this can be achieved as follows when using a [`Marten::Handlers::Template`](./generic-handlers.md#rendering-a-template) subclass: - def get - respond "Hello, authenticated user!" - end +```crystal +class MyHandler < Marten::Handlers::Template + template_name "app/my_template.html" + before_render :add_variable_to_context - private def add_required_header : Nil - response!.headers["X-Foo"] = "Bar" + private def add_variable_to_context : Nil + context["foo"] = "bar" end end ``` -Similarly to `#before_dispatch` callbacks, `#after_dispatch` callbacks can return a brand new [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) object. When this is the case, this response is always used instead of the one that was returned by the handler's `#dispatch` method. - ### Returning errors It is easy to forge any error response by leveraging the `#respond` or `#head` helpers that were mentioned [previously](#response-helper-methods). Using these helpers, it is possible to forge HTTP responses that are associated with specific error status codes and specific contents. For example: @@ -208,7 +229,7 @@ class MyHandler < Marten::Handler end ``` -It should be noted that Marten also support a couple of exceptions that can be raised to automatically trigger default error handlers. For example [`Marten::HTTP::Errors::NotFound`](pathname:///api/0.2/Marten/HTTP/Errors/NotFound.html) can be raised from any handler to force a 404 Not Found response to be returned. Default error handlers can be returned automatically by the framework in many situations (eg. a record is not found, or an unhandled exception is raised); you can learn more about them in [Error handlers](./error-handlers.md). +It should be noted that Marten also support a couple of exceptions that can be raised to automatically trigger default error handlers. For example [`Marten::HTTP::Errors::NotFound`](pathname:///api/0.5/Marten/HTTP/Errors/NotFound.html) can be raised from any handler to force a 404 Not Found response to be returned. Default error handlers can be returned automatically by the framework in many situations (eg. a record is not found, or an unhandled exception is raised); you can learn more about them in [Error handlers](./error-handlers.md). ## Mapping handlers to URLs @@ -228,7 +249,7 @@ Please refer to [Routing](./routing.md) for more information regarding routes co Handlers are able to interact with a cookies store, that you can use to store small amounts of data on the client. This data will be persisted across requests, and will be made accessible with every incoming request. -The cookies store is an instance of [`Marten::HTTP::Cookies`](pathname:///api/0.2/Marten/HTTP/Cookies.html) and provides a hash-like interface allowing to retrieve and store data. Handlers can access it through the use of the `#cookies` method. Here is a very simple example of how to interact with cookies: +The cookies store is an instance of [`Marten::HTTP::Cookies`](pathname:///api/0.5/Marten/HTTP/Cookies.html) and provides a hash-like interface allowing to retrieve and store data. Handlers can access it through the use of the `#cookies` method. Here is a very simple example of how to interact with cookies: ```crystal class MyHandler < Marten::Handler @@ -253,11 +274,13 @@ cookies.encrypted[:secret_message] = "Hello!" cookies.signed[:signed_message] = "Hello!" ``` +Please refer to [Cookies](./cookies.md) for more information around using cookies. + ## Using sessions Handlers can interact with a session store, which you can use to store small amounts of data that will be persisted between requests. How much data you can persist in this store depends on the session backend being used. The default backend persists session data using an encrypted cookie. Cookies have a 4K size limit, which is usually sufficient in order to persist things like a user ID and flash messages. -The session store is an instance of [`Marten::HTTP::Session::Store::Base`](pathname:///api/0.2/Marten/HTTP/Session/Store/Base.html) and provides a hash-like interface. Handlers can access it through the use of the `#session` method. For example: +The session store is an instance of [`Marten::HTTP::Session::Store::Base`](pathname:///api/0.5/Marten/HTTP/Session/Store/Base.html) and provides a hash-like interface. Handlers can access it through the use of the `#session` method. For example: ```crystal class MyHandler < Marten::Handler @@ -274,7 +297,7 @@ Please refer to [Sessions](./sessions.md) for more information regarding configu The flash store provides a way to pass basic string messages from one handler to the next one. Any string value that is set in this store will be available to the next handler processing the next request, and then it will be cleared out. Such mechanism provides a convenient way of creating one-time notification messages (such as alerts or notices). -The flash store is an instance [`Marten::HTTP::FlashStore`](pathname:///api/0.2/Marten/HTTP/FlashStore.html) and provides a hash-like interface. Handlers can access it through the use of the `#flash` method. For example: +The flash store is an instance [`Marten::HTTP::FlashStore`](pathname:///api/0.5/Marten/HTTP/FlashStore.html) and provides a hash-like interface. Handlers can access it through the use of the `#flash` method. For example: ```crystal class MyHandler < Marten::Handler @@ -300,3 +323,46 @@ The reverse operation is also possible: you can decide to discard all the curren flash.discard # discards all the flash messages flash.discard(:foo) # discards the message associated with the "foo" key only ``` + +## Streaming responses + +The [`Marten::HTTP::Response::Streaming`](pathname:///api/0.5/Marten/HTTP/Response/Streaming.html) response class gives you the ability to stream a response from Marten to the browser. However, unlike a standard response, this specialized class requires initialization from an [iterator](https://crystal-lang.org/api/Iterator.html) of strings instead of a content string. This approach proves to be beneficial if you intend to generate lengthy responses or responses that consume excessive memory (a classic example of this is the generation of large CSV files). + +Compared to a regular [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object, the [`Marten::HTTP::Response::Streaming`](pathname:///api/0.5/Marten/HTTP/Response/Streaming.html) class operates differently in two ways: + +* Instead of initializing it with a content string, it requires initialization from an [iterator](https://crystal-lang.org/api/Iterator.html) of strings. +* The response content is not directly accessible. The only way to obtain the actual response content is by iterating through the streamed content iterator, which can be accessed through the [`Marten::HTTP::Response::Streaming#streamed_content`](pathname:///api/0.5/Marten/HTTP/Response/Streaming.html#streamed_content%3AIterator(String)-instance-method) method. However, this is handled by Marten itself when sending the response to the browser, so you shouldn't need to worry about it. + +To generate streaming responses, you can either instantiate [`Marten::HTTP::Response::Streaming`](pathname:///api/0.5/Marten/HTTP/Response/Streaming.html) objects directly, or you can also leverage the [`#respond`](pathname:///api/0.5/Marten/Handlers/Base.html#respond(streamed_content%3AIterator(String)%2Ccontent_type%3DHTTP%3A%3AResponse%3A%3ADEFAULT_CONTENT_TYPE%2Cstatus%3D200)-instance-method) helper method, which works similarly to the [`#respond`](#respond) variant for response content strings. + +For example, the following handler generates a CSV and streams its content by leveraging the [`#respond`](pathname:///api/0.5/Marten/Handlers/Base.html#respond(streamed_content%3AIterator(String)%2Ccontent_type%3DHTTP%3A%3AResponse%3A%3ADEFAULT_CONTENT_TYPE%2Cstatus%3D200)-instance-method) helper method: + +```crystal +require "csv" + +class StreamingTestHandler < Marten::Handler + def get + respond(streaming_iterator, content_type: "text/csv") + end + + private def streaming_iterator + csv_io = IO::Memory.new + csv_builder = CSV::Builder.new(io: csv_io) + + (1..1000000).each.map do |idx| + csv_builder.row("Row #{idx}", "Val #{idx}") + + row_content = csv_io.to_s + + csv_io.rewind + csv_io.flush + + row_content + end + end +end +``` + +:::caution +When considering streaming responses, it is crucial to understand that the process of streaming ties up a worker process for the entire response duration. This can significantly impact your worker's performance, so it's essential to use this approach only when necessary. Generally, it's better to carry out expensive content generation tasks outside the request-response cycle to avoid any negative impact on your worker's performance. +::: diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/middlewares.md b/docs/versioned_docs/version-0.5/handlers-and-http/middlewares.md similarity index 92% rename from docs/versioned_docs/version-0.2/handlers-and-http/middlewares.md rename to docs/versioned_docs/version-0.5/handlers-and-http/middlewares.md index bf501e116..5e3a78d9c 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/middlewares.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/middlewares.md @@ -9,7 +9,7 @@ Middlewares are used to "hook" into Marten's request/response lifecycle. They ca ## How middlewares work -Middlewares are subclasses of the [`Marten::Middleware`](pathname:///api/0.2/Marten/Middleware.html) abstract class. They must implement a `#call` method that takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.2/Marten/HTTP/Request.html)) and a `get_response` proc (allowing to get the final response) as arguments, and that returns a [`Marten::HTTP::Response`](pathname:///api/0.2/Marten/HTTP/Response.html) object: +Middlewares are subclasses of the [`Marten::Middleware`](pathname:///api/0.5/Marten/Middleware.html) abstract class. They must implement a `#call` method that takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.5/Marten/HTTP/Request.html)) and a `get_response` proc (allowing to get the final response) as arguments, and that returns a [`Marten::HTTP::Response`](pathname:///api/0.5/Marten/HTTP/Response.html) object: ```crystal class TestMiddleware < Marten::Middleware diff --git a/docs/versioned_docs/version-0.5/handlers-and-http/reference/generic-handlers.md b/docs/versioned_docs/version-0.5/handlers-and-http/reference/generic-handlers.md new file mode 100644 index 000000000..5035e8bd4 --- /dev/null +++ b/docs/versioned_docs/version-0.5/handlers-and-http/reference/generic-handlers.md @@ -0,0 +1,328 @@ +--- +title: Generic handlers +description: Generic handlers reference +--- + +This page provides a reference for all the available [generic handlers](../generic-handlers.md). + +## Creating a record + +**Class:** [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) + +Handler allowing to create a new model record by processing a schema. + +This handler can be used to process a form, validate its data through the use of a [schema](../../schemas.mdx), and create a record by using the validated data. It is expected that the handler will be accessed through a GET request first: when this happens the configured template is rendered and displayed, and the configured schema which is initialized can be accessed from the template context in order to render a form for example. When the form is submitted via a POST request, the configured schema is validated using the form data. If the data is valid, the corresponding model record is created and the handler returns an HTTP redirect to a configured success URL. + +```crystal +class MyFormHandler < Marten::Handlers::RecordCreate + model MyModel + schema MyFormSchema + template_name "my_form.html" + success_route_name "my_form_success" +end +``` + +It should be noted that the redirect response issued will be a 302 (found). + +The model class used to create the new record can be configured through the use of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html#model(model_klass)-macro) macro. The schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.5/Marten/Handlers/Schema.html#schema(schema_klass)-macro) macro. Alternatively, the [`#schema_class`](pathname:///api/0.5/Marten/Handlers/Schema.html#schema_class-instance-method) method can also be overridden to dynamically define the schema class as part of the request handler handling. + +The [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the schema while the [`#success_route_name`](pathname:///api/0.5/Marten/Handlers/Schema.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the schema has been validated. Alternatively, the [`#sucess_url`](pathname:///api/0.5/Marten/Handlers/Schema.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.5/Marten/Handlers/Schema.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. + +For example, if your application logic requires a success route that includes an identifier (such as a record's primary key), you can customize the success URL by overriding the `#success_url` method like this: + +```crystal +def success_url + reverse("article", pk: record.pk!) # record is an instance of the model you defined for your handler +end +``` + +:::tip +Handlers making use of the [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) generic handler can leverage additional types of callbacks. Please head over to [Schema handler callbacks](../callbacks.md#schema-handler-callbacks) to learn more about those. +::: + +## Deleting a record + +**Class:** [`Marten::Handlers::RecordDelete`](pathname:///api/0.5/Marten/Handlers/RecordDelete.html) + +Handler allowing to delete a specific model record. + +This handler can be used to delete an existing model record by issuing a POST request. Optionally the handler can be accessed with a GET request and a template can be displayed in this case; this allows to display a confirmation page to users before deleting the record: + +```crystal +class ArticleDeleteHandler < Marten::Handlers::RecordDelete + model MyModel + template_name "article_delete.html" + success_route_name "article_delete_success" +end +``` + +It should be noted that the redirect response issued will be a 302 (found). + +The [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render a deletion confirmation page while the [`#success_route_name`](pathname:///api/0.5/Marten/Handlers/RecordDelete.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the deletion is complete. Alternatively, the [`#sucess_url`](pathname:///api/0.5/Marten/Handlers/RecordDelete.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.5/Marten/Handlers/RecordDelete.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. It's also possible to pre-filter the queryset before deleting the record by using the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro. + +:::tip How to customize the query set? +By default, handlers that inherit from [`Marten::Handlers::RecordDelete`](pathname:///api/0.5/Marten/Handlers/RecordDelete.html) will use a query set targetting _all_ the records in order to retrieve the record that should be deleted. It should be noted that you can customize this behavior easily by leveraging the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro instead of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#model(model_klass)-macro) macro. For example: + +```crystal +class ArticleDeleteHandler < Marten::Handlers::RecordDelete + queryset MyModel.filter(user: request.user) + template_name "article_delete.html" + success_route_name "article_delete_success" +end +``` + +Alternatively, it is also possible to override the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset-instance-method) method and apply additional filters to the default query set: + +```crystal +class ArticleDeleteHandler < Marten::Handlers::RecordDelete + model MyModel + template_name "article_delete.html" + success_route_name "article_delete_success" + + def queryset + super.filter(user: request.user) + end +end +``` +::: + +## Displaying a record + +**Class:** [`Marten::Handlers::RecordDetail`](pathname:///api/0.5/Marten/Handlers/RecordDetail.html) + +Handler allowing to display a specific model record. + +This handler can be used to retrieve a [model](../../models-and-databases/introduction.md) record, and to display it as part of a [rendered template](../../templates.mdx). + +```crystal +class ArticleDetailHandler < Marten::Handlers::RecordDetail + model Article + template_name "articles/detail.html" +end +``` + +The model class used to retrieve the record can be configured through the use of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#model(model_klass)-macro) macro. It's also possible to pre-filter the queryset before retrieving the record by using the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro. By default, a [`Marten::Handlers::RecordDetail`](pathname:///api/0.5/Marten/Handlers/RecordDetail.html) subclass will always retrieve model records by looking for a `pk` route parameter: this parameter is assumed to contain the value of the primary key field associated with the record that should be rendered. If you need to use a different route parameter name, you can also specify a different one through the use of the [`#lookup_param`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. Finally, the model field that is used to get the model record (defaulting to `pk`) can also be configured by leveraging the [`#lookup_param`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. + +The [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the considered model record. By default, the model record is associated with a `record` key in the template context, but this can also be configured by using the [`record_context_name`](pathname:///api/0.5/Marten/Handlers/RecordDetail.html#record_context_name(name%3AString|Symbol)-class-method) class method. + +:::tip How to customize the query set? +By default, handlers that inherit from [`Marten::Handlers::RecordDetail`](pathname:///api/0.5/Marten/Handlers/RecordDetail.html) will use a query set targetting _all_ the records in order to retrieve the record that should be displayed. It should be noted that you can customize this behavior easily by leveraging the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro instead of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#model(model_klass)-macro) macro. For example: + +```crystal +class ArticleDetailHandler < Marten::Handlers::RecordDetail + queryset Article.filter(user: request.user) + template_name "articles/detail.html" +end +``` + +Alternatively, it is also possible to override the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset-instance-method) method and apply additional filters to the default query set: + +```crystal +class ArticleDetailHandler < Marten::Handlers::RecordDetail + model Article + template_name "articles/detail.html" + + def queryset + super.filter(user: request.user) + end +end +``` +::: + +## Listing records + +**Class:** [`Marten::Handlers::RecordList`](pathname:///api/0.5/Marten/Handlers/RecordList.html) + +Handler allowing to list model records. + +This base handler can be used to easily expose a list of model records: + +```crystal +class MyHandler < Marten::Handlers::RecordList + template_name "my_template" + model Post +end +``` + +The model class used to retrieve the records can be configured through the use of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordListing.html#model(model_klass)-macro) macro. The [order](../../models-and-databases/reference/query-set.md#order) of these model records can also be specified by leveraging the [`#ordering`](pathname:///api/0.5/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. + +The [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the list of model records. By default, the list of model records is associated with a `records` key in the template context, but this can also be configured by using the [`list_context_name`](pathname:///api/0.5/Marten/Handlers/RecordList.html#list_context_name(name%3AString|Symbol)-class-method) class method. + +Optionally, it is possible to configure that records should be [paginated](../../models-and-databases/reference/query-set.md#paginator) by specifying a page size through the use of the [`page_size`](pathname:///api/0.5/Marten/Handlers/RecordListing/ClassMethods.html#page_size(page_size%3AInt32%3F)-instance-method) class method: + +```crystal +class MyHandler < Marten::Handlers::RecordList + template_name "my_template" + model Post + page_size 12 +end +``` + +When records are paginated, a [`Marten::DB::Query::Page`](pathname:///api/0.5/Marten/DB/Query/Page.html) object will be exposed in the template context (instead of the raw query set). It should be noted that the page number that should be displayed is determined by looking for a `page` GET parameter by default; this parameter name can be configured as well by calling the [`page_number_param`](pathname:///api/0.5/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. + +:::tip How to customize the query set? +By default, handlers that inherit from [`Marten::Handlers::RecordList`](pathname:///api/0.5/Marten/Handlers/RecordList.html) will use a query set targetting _all_ the records of the specified model. It should be noted that you can customize this behavior easily by leveraging the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordListing.html#queryset(queryset)-macro) macro instead of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordListing.html#model(model_klass)-macro) macro. For example: + +```crystal +class MyHandler < Marten::Handlers::RecordList + template_name "my_template" + queryset Article.filter(user: request.user) +end +``` + +Alternatively, it is also possible to override the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordListing.html#queryset-instance-method) method and apply additional filters to the default query set: + +```crystal +class MyHandler < Marten::Handlers::RecordList + template_name "my_template" + model Article + + def queryset + super.filter(user: request.user) + end +end +``` +::: + +## Updating a record + +**Class:** [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html) + +Handler allowing to update a model record by processing a schema. + +This handler can be used to process a form, validate its data through the use of a [schema](../../schemas.mdx), and update an existing record by using the validated data. It is expected that the handler will be accessed through a GET request first: when this happens the configured template is rendered and displayed, and the configured schema which is initialized can be accessed from the template context to render a form for example. When the form is submitted via a POST request, the configured schema is validated using the form data. If the data is valid, the model record that was retrieved is updated and the handler returns an HTTP redirect to a configured success URL. + +```crystal +class MyUpdateHandler < Marten::Handlers::RecordUpdate + model MyModel + schema MyUpdateSchema + template_name "my_form.html" + success_route_name "my_form_success" +end +``` + +It should be noted that the redirect response issued will be a 302 (found). + +The model class used to update the new record can be configured through the use of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#model(model_klass)-macro) macro. It's also possible to pre-filter the queryset before updating the record by using the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro. By default, the record to update is retrieved by expecting a `pk` route parameter: this parameter is assumed to contain the value of the primary key field associated with the record that should be updated. If you need to use a different route parameter name, you can also specify a different one through the use of the [`#lookup_param`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. Finally, the model field that is used to get the model record (defaulting to `pk`) can also be configured by leveraging the [`#lookup_param`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. + +The schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.5/Marten/Handlers/Schema.html#schema(schema_klass)-macro) macro. Alternatively, the [`#schema_class`](pathname:///api/0.5/Marten/Handlers/Schema.html#schema_class-instance-method) method can also be overridden to dynamically define the schema class as part of the request handler handling. + +The [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the schema while the [`#success_route_name`](pathname:///api/0.5/Marten/Handlers/Schema.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the schema has been validated. Alternatively, the [`#sucess_url`](pathname:///api/0.5/Marten/Handlers/Schema.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.5/Marten/Handlers/Schema.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. + +:::tip +Handlers making use of the [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html) generic handler can leverage additional types of callbacks. Please head over to [Schema handler callbacks](../callbacks.md#schema-handler-callbacks) to learn more about those. +::: + +:::tip How to customize the query set? +By default, handlers that inherit from [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html) will use a query set targetting _all_ the records in order to retrieve the record that should be updated. It should be noted that you can customize this behavior easily by leveraging the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro instead of the [`#model`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#model(model_klass)-macro) macro. For example: + +```crystal +class MyUpdateHandler < Marten::Handlers::RecordUpdate + queryset MyModel.filter(user: request.user) + schema MyUpdateSchema + template_name "my_form.html" + success_route_name "my_form_success" +end +``` + +Alternatively, it is also possible to override the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset-instance-method) method and apply additional filters to the default query set: + +```crystal +class MyUpdateHandler < Marten::Handlers::RecordUpdate + model MyModel + schema MyUpdateSchema + template_name "my_form.html" + success_route_name "my_form_success" + + def queryset + super.filter(user: request.user) + end +end +``` +::: + + +## Performing a redirect + +**Class:** [`Marten::Handlers::Redirect`](pathname:///api/0.5/Marten/Handlers/Redirect.html) + +Handler allowing to conveniently return redirect responses. + +This handler can be used to generate a redirect response (temporary or permanent) to another location. To configure such a location, you can either leverage the [`#route_name`](pathname:///api/0.5/Marten/Handlers/Redirect.html#route_name(route_name%3AString%3F)-class-method) class method (which expects a valid [route name](../routing.md#reverse-url-resolutions)) or the [`#url`](pathname:///api/0.5/Marten/Handlers/Redirect.html#url(url%3AString%3F)-class-method) class method. If you need to implement a custom redirection URL logic, you can also override the [`#redirect_url`](pathname:///api/0.5/Marten/Handlers/Redirect.html#redirect_url-instance-method) method. + +```crystal +class TestRedirectHandler < Marten::Handlers::Redirect + route_name "articles:list" +end +``` + +By default, the redirect returned by this handler is a temporary one. In order to generate a permanent redirect response instead, it is possible to leverage the [`#permanent`](pathname:///api/0.5/Marten/Handlers/Redirect.html#permanent(permanent%3ABool)-class-method) class method. + +It should also be noted that by default, incoming query string parameters **are not** forwarded to the redirection URL. If you wish to ensure that these parameters are forwarded, you can make use of the [`forward_query_string`](pathname:///api/0.5/Marten/Handlers/Redirect.html#forward_query_string(forward_query_string%3ABool)-class-method) class method. + +## Processing a schema + +**Class:** [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) + +Handler allowing to process a form through the use of a [schema](../../schemas.mdx). + +This handler can be used to process a form and validate its data through the use of a [schema](../../schemas.mdx). It is expected that the handler will be accessed through a GET request first: when this happens the configured template is rendered and displayed, and the configured schema which is initialized can be accessed from the template context to render a form for example. When the form is submitted via a POST request, the configured schema is validated using the form data. If the data is valid, the handler returns an HTTP redirect to a configured success URL. + +```crystal +class MyFormHandler < Marten::Handlers::Schema + schema MyFormSchema + template_name "my_form.html" + success_route_name "my_form_success" +end +``` + +It should be noted that the redirect response issued will be a 302 (found). + +The schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.5/Marten/Handlers/Schema.html#schema(schema_klass)-macro) macro. Alternatively, the [`#schema_class`](pathname:///api/0.5/Marten/Handlers/Schema.html#schema_class-instance-method) method can also be overridden to dynamically define the schema class as part of the request handler handling. + +The [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method allows defining the name of the template to use to render the schema while the [`#success_route_name`](pathname:///api/0.5/Marten/Handlers/Schema.html#success_route_name(success_route_name%3AString%3F)-class-method) method can be used to specify the name of a route to redirect to once the schema has been validated. Alternatively, the [`#sucess_url`](pathname:///api/0.5/Marten/Handlers/Schema.html#success_url(success_url%3AString%3F)-class-method) class method can be used to provide a raw URL to redirect to. The [same method](pathname:///api/0.5/Marten/Handlers/Schema.html#success_url-instance-method) can also be overridden at the instance level to rely on a custom logic to generate the success URL to redirect to. + +:::tip +Handlers making use of the [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) generic handler can leverage additional types of callbacks. Please head over to [Schema handler callbacks](../callbacks.md#schema-handler-callbacks) to learn more about those. +::: + +## Rendering a template + +**Class:** [`Marten::Handlers::Template`](pathname:///api/0.5/Marten/Handlers/Template.html) + +Handler allowing to respond to `GET` request with the content of a rendered HTML [template](../../templates.mdx). + +This handler can be used to render a specific template and returns the resulting content in the response. The template being rendered can be specified by leveraging the [`#template_name`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method. + +```crystal +class HomeHandler < Marten::Handlers::Template + template_name "app/home.html" +end +``` + +If you need to, it is possible to customize the context that is used to render the configured template. To do so, you can define a [`before_render`](../callbacks.md#before_render) callback and add new variables to the [global template context](../introduction.md#global-template-context) (which functions similarly to a hash object): + +```crystal +class HomeHandler < Marten::Handlers::Template + template_name "app/home.html" + + before_render add_recent_articles_to_context + + private def add_recent_articles_to_context : Nil + context[:recent_articles] = Article.all.order("-published_at")[:5] + end +end +``` + +Variables that are added to the global template context will automatically be available to the configured template's runtime. + +:::tip +The default content type of the response generated when rendering templates is `text/html`, but this can be customized using the [`#content_type`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#content_type(content_type%3AString|Nil)-instance-method) class method. For example: + +```crystal +class MyHandler < Marten::Handlers::Template + template_name "app/test.xml" + content_type "application/xml" +end +``` +::: diff --git a/docs/versioned_docs/version-0.5/handlers-and-http/reference/middlewares.md b/docs/versioned_docs/version-0.5/handlers-and-http/reference/middlewares.md new file mode 100644 index 000000000..95ca9699a --- /dev/null +++ b/docs/versioned_docs/version-0.5/handlers-and-http/reference/middlewares.md @@ -0,0 +1,133 @@ +--- +title: Middlewares +description: Middlewares reference +--- + +This page provides a reference for all the available [middlewares](../middlewares.md). + +## Asset serving middleware + +**Class:** [`Marten::Middleware::AssetServing`](pathname:///api/0.5/Marten/Middleware/AssetServing.html) + +The purpose of this middleware is to handle the distribution of collected assets, which are stored under the configured assets root ([`assets.root`](../../development/reference/settings.md#root) setting). The assumption is that these assets have been "collected" using the [`collectassets`](../../development/reference/management-commands.md#collectassets) management command and that the file system storage ([`Marten::Core::Storage::FileSystem`](pathname:///api/0.5/Marten/Core/Storage/FileSystem.html)) is being used. + +Additionally, the [`assets.url`](../../development/reference/settings.md#url) setting must either align with the domain of your Marten application or correspond to a relative URL path, such as `/assets/`. This ensures proper mapping and accessibility of the assets within your application (so that they can be served by this middleware). + +It is important to mention that this middleware automatically applies compression to the served assets, utilizing GZip or deflate based on the Accept-Encoding header of the incoming request. Additionally, the middleware sets the Cache-Control header and defines a max-age of 3600 seconds, ensuring efficient caching of the assets. + +:::info +This middleware should be placed at the first position in the [`middleware`](../../development/reference/settings.md#middleware) setting (ie. before all other configured middlewares). +::: + +:::tip +This middleware is provided to make it easy to serve assets in situations where you can't easily configure a web server such as [Nginx](https://nginx.org) or a third-party service (like Amazon's S3 or GCS) to serve your assets directly. +::: + +## Content-Security-Policy middleware + +**Class:** [`Marten::Middleware::ContentSecurityPolicy`](pathname:///api/0.5/Marten/Middleware/ContentSecurityPolicy.html) + +This middleware guarantees the presence of the Content-Security-Policy header in the response's headers. This header provides clients with the ability to limit the allowed sources of different types of content. + +By default, the middleware will include a Content-Security-Policy header that corresponds to the policy defined in the [`content_security_policy`](../../development/reference/settings.md#content-security-policy-settings) settings. However, if a [`Marten::HTTP::ContentSecurityPolicy`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html) object is explicitly assigned to the request object, it will take precedence over the default policy and be used instead. + +Please refer to [Content Security Policy](../../security/content-security-policy.md) to learn more about the Content-Security-Policy header and how to configure it. + +## Flash middleware + +**Class:** [`Marten::Middleware::Flash`](pathname:///api/0.5/Marten/Middleware/Flash.html) + +Enables the use of [flash messages](../introduction.md#using-the-flash-store). + +When this middleware is used, each request will have a flash store initialized and populated from the request's session store. This flash store is a hash-like object that allows to fetch or set values that are associated with specific keys, and that will only be available to the next request (after that they are cleared out). + +The flash store depends on the presence of a working session store. As such, the [Session middleware](#session-middleware) MUST be used along with this middleware. Moreover, this middleware must be placed _after_ the [`Marten::Middleware::Session`](pathname:///api/0.5/Marten/Middleware/Session.html) in the [`middleware`](../../development/reference/settings.md#middleware) setting. + +## GZip middleware + +**Class:** [`Marten::Middleware::GZip`](pathname:///api/0.5/Marten/Middleware/GZip.html) + +Compresses the content of the response if the browser supports GZip compression. + +This middleware will compress responses that are big enough (200 bytes or more) if they don't already contain an Accept-Encoding header. It will also set the Vary header correctly by including Accept-Encoding in it so that caches take into account the fact that the content can be compressed or not. + +The GZip middleware should be positioned before any other middleware that needs to interact with the response content in the [`middleware`](../../development/reference/settings.md#middleware) setting. This is to ensure that the compression happens only when the response content is no longer accessed. + +:::note +The GZip middleware incorporates a mitigation strategy against the [BREACH attack](https://www.breachattack.com/). This strategy (described in the [Heal The Breach paper](https://ieeexplore.ieee.org/document/9754554)) involves introducing up to 100 random bytes into GZip responses to enhance the security against such attacks. +::: + +## I18n middleware + +**Class:** [`Marten::Middleware::I18n`](pathname:///api/0.5/Marten/Middleware/I18n.html) + +Activates the right I18n locale based on incoming requests. + +This middleware will activate the right locale based on the Accept-Language header or the value provided by the [locale cookie](../../development/reference/settings.md#locale_cookie_name). Only explicitly-configured locales can be activated by this middleware (that is, locales that are specified in the [`i18n.available_locales`](../../development/reference/settings.md#available_locales) and [`i18n.default_locale`](../../development/reference/settings.md#default_locale) settings). If the incoming locale can't be found in the project configuration, the default locale will be used instead. + +## Method Override middleware + +**Class:** [`Marten::Middleware::MethodOverride`](pathname:///api/0.5/Marten/Middleware/MethodOverride.html) + +This middleware enables support for overriding HTTP methods in HTML forms that natively only support GET and POST. It does this by inspecting requests and looking for a `_method` parameter, allowing to simulate methods like PUT, DELETE, and others. It's also possible to change the parameter name and the allowed override methods in the [method override configuration](../../development/reference/settings.md#method-overriding-settings). + +For example: + +```html +
+ + +
+``` + +With the `MethodOverride` middleware, the form submission would effectively be treated as a `DELETE` request instead of `POST`. + +:::info +The middleware should be placed as far as possible at the beginning of the array of the [`middlewares`](../../development/reference/settings.md#middleware) setting so that other middlewares already recognise the overridden method. +::: + +## Session middleware + +**Class:** [`Marten::Middleware::Session`](pathname:///api/0.5/Marten/Middleware/Session.html) + +Enables the use of [sessions](../sessions.md). + +When this middleware is used, each request will have a session store initialized according to the [sessions configuration](../../development/reference/settings.md#sessions-settings). This session store is a hash-like object that allows to fetch or set values that are associated with specific keys. + +The session store is initialized from a session key that is stored as a regular cookie. If the session store ends up being empty after a request's handling, the associated cookie is deleted. Otherwise, the cookie is refreshed if the session store is modified as part of the considered request. Each session cookie is set to expire according to a configured cookie max age (the default cookie max age is 2 weeks). + +## SSL redirect middleware + +**Class:** [`Marten::Middleware::SSLRedirect`](pathname:///api/0.5/Marten/Middleware/SSLRedirect.html) + +Redirects all non-HTTPS requests to HTTPS. + +This middleware will permanently redirect all non-HTTP requests to HTTPS. By default the middleware will redirect to the incoming request's host, but a different host to redirect to can be configured with the [`ssl_redirect.host`](../../development/reference/settings.md#host-2) setting. Additionally, specific request paths can also be exempted from this SSL redirect if the corresponding strings or regexes are specified in the [`ssl_redirect.exempted_paths`](../../development/reference/settings.md#exempted_paths) setting. + +## Strict-Transport-Security middleware + +**Class:** [`Marten::Middleware::StrictTransportSecurity`](pathname:///api/0.5/Marten/Middleware/StrictTransportSecurity.html) + +Sets the Strict-Transport-Security header in the response if it wasn't already set. + +This middleware automatically sets the HTTP Strict-Transport-Security (HSTS) response header for all responses unless it was already specified in the response headers. This allows to let browsers know that the considered website should only be accessed using HTTPS, which results in future HTTP requests being automatically converted to HTTPS (up until the configured strict transport policy max age is reached). + +Browsers ensure that this policy is applied for a specific duration because a `max-age` directive is embedded into the header value. This max age duration is expressed in seconds and can be configured using the [`strict_security_policy.max_age`](../../development/reference/settings.md#max_age) setting. + +:::caution +When enabling this middleware, you should probably start with small values for the [`strict_security_policy.max_age`](../../development/reference/settings.md#max_age) setting (for example `3600` - one hour). Indeed, when browsers are aware of the Strict-Transport-Security header they will refuse to connect to your website using HTTP until the expiry time corresponding to the configured max age is reached. + +This is why the value of the [`strict_security_policy.max_age`](../../development/reference/settings.md#max_age) setting is `nil` by default: this prevents the middleware from inserting the Strict-Transport-Security response header until you actually specify a max age. +::: + +## X-Frame-Options middleware + +**Class:** [`Marten::Middleware::XFrameOptions`](pathname:///api/0.5/Marten/Middleware/XFrameOptions.html) + +Sets the X-Frame-Options header in the response if it wasn't already set. + +When this middleware is used, a X-Frame-Options header will be inserted into the HTTP response. The default value for this header (which is configurable via the [`x_frame_options`](../../development/reference/settings.md#x_frame_options) setting) is "DENY", which means that the response cannot be displayed in a frame. This allows preventing click-jacking attacks, by ensuring that the web app cannot be embedded into other sites. + +On the other hand, if the `x_frame_options` is set to "SAMEORIGIN", the page can be displayed in a frame if the including site is the same as the one serving the page. diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/routing.md b/docs/versioned_docs/version-0.5/handlers-and-http/routing.md similarity index 82% rename from docs/versioned_docs/version-0.2/handlers-and-http/routing.md rename to docs/versioned_docs/version-0.5/handlers-and-http/routing.md index fb6a80c3a..2c89dfe22 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/routing.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/routing.md @@ -107,21 +107,61 @@ In the above example, the following URLs would be generated by Marten in additio As you can see, both the URLs and the route names end up being prefixed respectively with the path and the name specified in the including route. -Note that the sub routes map does not have to live in the `config/routes.cr` file: it can technically live anywhere in your codebase. The ideal way to define the routes map of a specific application would be to put it in a `routes.cr` file in the application's directory. +:::info +The `name` parameter for included routes is optional, i.e. `path "/articles", ARTICLE_ROUTES` is also valid. Please note that this will increase the possibility of a name collision and it is, therefore, advisable to prefix the individual paths of the included route, e.g. `article_list`, `article_create`, etc. -When Marten encounters a path that leads to another sub routes map, it chops off the part of the URL that was matched up to that point and then forwards the remaining to the sub routes map in order to see if it is matched by one of the underlying routes. +```crystal +ARTICLE_ROUTES = Marten::Routing::Map.draw do + path "/", ArticlesHandler, name: "article_list" + path "/create", ArticlesCreateHandler, name: "article_create" +end + +Marten.routes.draw do + path "/articles", ARTICLE_ROUTES +end +``` + +This example will generate the following URLs: + +| URL | Handler | Name | +| --- | ------- | ---- | +| `/articles` | `ArticlesHandler` | `articles_list` | +| `/articles/create` | `ArticlesCreateHandler` | `articles_create` | + +It is also possible to add a namespace to the included route at the map level: + +```crystal +ARTICLE_ROUTES = Marten::Routing::Map.draw(:article) do + path "/", ArticlesHandler, name: "list" +end + +Marten.routes.draw do + path "/articles", ARTICLE_ROUTES # Note: providing the name parameter overrides the namespace +end +``` + +This example will generate the following URLs: + +| URL | Handler | Name | +| --- | ------- | ---- | +| `/articles` | `ArticlesHandler` | `articles:list` | +::: + +Note that the sub-routes map does not have to live in the `config/routes.cr` file: it can technically live anywhere in your codebase. The ideal way to define the routes map of a specific application would be to put it in a `routes.cr` file in the application's directory. + +When Marten encounters a path that leads to another sub-routes map, it chops off the part of the URL that was matched up to that point and then forwards the remaining to the sub-routes map in order to see if it is matched by one of the underlying routes. ## Reverse URL resolutions When working with web applications, a frequent need is to generate URLs in their final forms. To do so, you will want to avoid hard-coding URLs and instead leverage the ability to generate them from their associated names: this is what we call a reverse URL resolution. -"Reversing" a URL is as simple as calling the [`Marten::Routing::Map#reverse`](pathname:///api/0.2/Marten/Routing/Map.html#reverse(name%3AString|Symbol%2Cparams%3AHash(String|Symbol%2CParameter%3A%3ATypes))-instance-method) method from the main routes map, which is accessible through the use of the [`Marten#routes`](pathname:///api/0.2/Marten.html#routes-class-method) method: +"Reversing" a URL is as simple as calling the [`Marten::Routing::Map#reverse`](pathname:///api/0.5/Marten/Routing/Map.html#reverse(name%3AString|Symbol%2Cparams%3AHash(String|Symbol%2CParameter%3A%3ATypes))-instance-method) method from the main routes map, which is accessible through the use of the [`Marten#routes`](pathname:///api/0.5/Marten.html#routes-class-method) method: ```crystal Marten.routes.reverse("home") # will return "/" ``` -In order to reverse a URL from within a handler class, you can simply leverage the [`Marten::Handlers::Base#reverse`](pathname:///api/0.2/Marten/Handlers/Base.html#reverse(*args%2C**options)-instance-method) handler method: +In order to reverse a URL from within a handler class, you can simply leverage the [`Marten::Handlers::Base#reverse`](pathname:///api/0.5/Marten/Handlers/Base.html#reverse(*args%2C**options)-instance-method) handler method: ```crystal class MyHandler < Marten::Handler diff --git a/docs/versioned_docs/version-0.2/handlers-and-http/sessions.md b/docs/versioned_docs/version-0.5/handlers-and-http/sessions.md similarity index 72% rename from docs/versioned_docs/version-0.2/handlers-and-http/sessions.md rename to docs/versioned_docs/version-0.5/handlers-and-http/sessions.md index f3e4ab252..cb4e76c88 100644 --- a/docs/versioned_docs/version-0.2/handlers-and-http/sessions.md +++ b/docs/versioned_docs/version-0.5/handlers-and-http/sessions.md @@ -8,9 +8,9 @@ Sessions can be used to store small amounts of data that will be persisted betwe ## Configuration -In order to use sessions, you need to make sure that the [`Marten::Middleware::Session`](pathname:///api/0.2/Marten/Middleware/Session.html) middleware is part of your project's middleware chain, which can be configured in the [`middleware`](../development/reference/settings.md#middleware) setting. Note that the session middleware class is automatically added to this setting when initializing new projects. +In order to use sessions, you need to make sure that the [`Marten::Middleware::Session`](pathname:///api/0.5/Marten/Middleware/Session.html) middleware is part of your project's middleware chain, which can be configured in the [`middleware`](../development/reference/settings.md#middleware) setting. Note that the session middleware class is automatically added to this setting when initializing new projects. -If your project does not require the use of sessions, you can simply ensure that the [`middleware`](../development/reference/settings.md#middleware) setting does not include the [`Marten::Middleware::Session`](pathname:///api/0.2/Marten/Middleware/Session.html) middleware class. +If your project does not require the use of sessions, you can simply ensure that the [`middleware`](../development/reference/settings.md#middleware) setting does not include the [`Marten::Middleware::Session`](pathname:///api/0.5/Marten/Middleware/Session.html) middleware class. How the session ID cookie is generated can also be tweaked by leveraging the following settings: @@ -25,13 +25,17 @@ How the session ID cookie is generated can also be tweaked by leveraging the fol How session data is actually persisted can be defined by configuring the right session store backend, which can be done through the use of the [`sessions.store`](../development/reference/settings.md#store) setting. -By default, sessions are stored within a single cookie (`:cookie` session store). Cookies have a 4K size limit, which is usually sufficient in order to persist things like a user ID and flash messages. `:cookie` is the only store that is built in the Marten web framework presently. +By default, sessions are encrypted and stored within a single cookie (`:cookie` session store). Cookies have a 4K size limit, which is usually sufficient in order to persist things like a user ID and flash messages. `:cookie` is the only store that is built in the Marten web framework presently. -Other session stores can be installed as separate shards. For example, the [`marten-db-session`](https://github.com/martenframework/marten-db-session) shard can be leveraged to persist session data in the database. +:::info +The `cookie` store leverages a [`Marten::Core::Encryptor`](pathname:///api/0.5/Marten/Core/Encryptor.html) encryptor object in order to encrypt and sign session data. This means that session data is encrypted with an **aes-256-cbc** cipher and signed with HMAC signatures that use the **SHA256** hash algorithm. +::: + +Other session stores can be installed as separate shards. For example, the [`marten-db-session`](https://github.com/martenframework/marten-db-session) shard can be leveraged to persist session data in the database while the [`marten-redis-session`](https://github.com/martenframework/marten-redis-session) shard can be used for persisting session data using Redis. ## Using sessions -When the [`Marten::Middleware::Session`](pathname:///api/0.2/Marten/Middleware/Session.html) middleware is used, each HTTP request object will have a [`#session`](pathname:///api/0.2//Marten/HTTP/Request.html#session-instance-method) method returning the session store for the current request. The session store is an instance of [`Marten::HTTP::Session::Store::Base`](pathname:///api/0.2/Marten/HTTP/Session/Store/Base.html) and provides a hash-like interface: +When the [`Marten::Middleware::Session`](pathname:///api/0.5/Marten/Middleware/Session.html) middleware is used, each HTTP request object will have a [`#session`](pathname:///api/0.5//Marten/HTTP/Request.html#session-instance-method) method returning the session store for the current request. The session store is an instance of [`Marten::HTTP::Session::Store::Base`](pathname:///api/0.5/Marten/HTTP/Session/Store/Base.html) and provides a hash-like interface: ```crystal # Persisting values: diff --git a/docs/versioned_docs/version-0.2/i18n.mdx b/docs/versioned_docs/version-0.5/i18n.mdx similarity index 100% rename from docs/versioned_docs/version-0.2/i18n.mdx rename to docs/versioned_docs/version-0.5/i18n.mdx diff --git a/docs/versioned_docs/version-0.2/i18n/introduction.md b/docs/versioned_docs/version-0.5/i18n/introduction.md similarity index 90% rename from docs/versioned_docs/version-0.2/i18n/introduction.md rename to docs/versioned_docs/version-0.5/i18n/introduction.md index a78742ddd..3da1e5000 100644 --- a/docs/versioned_docs/version-0.2/i18n/introduction.md +++ b/docs/versioned_docs/version-0.5/i18n/introduction.md @@ -139,6 +139,18 @@ en: In this case, the `foo` application's codebase would request translations using the `foo.message` key, which makes it impossible to encounter conflict issues with other application translations. +## How Marten resolves the current locale + +Marten will attempt to determine the "current" locale for activation only when the [I18n middleware](../handlers-and-http/reference/middlewares.md#i18n-middleware) is used. + +This middleware can activate the appropriate locale by considering the following: + +* The value of the Accept-Language header. +* The value of a cookie, with its name defined by the [`i18n.locale_cookie_name`](../development/reference/settings.md#locale_cookie_name) setting. + + +The [I18n middleware](../handlers-and-http/reference/middlewares.md#i18n-middleware) only allows activation of explicitly configured locales, which are specified in the [`i18n.available_locales`](../development/reference/settings.md#available_locales) and [`i18n.default_locale`](../development/reference/settings.md#default_locale) settings. If the incoming locale is not found in the project configuration, the default locale will be used instead. By utilizing this middleware, you can be sure that the right locale is automatically enabled for your users, so that you don't need to take care of it. + ## Limitations It's important to be aware of a few limitations when working with translations powered by [Crystal I18n](https://crystal-i18n.github.io/) within a Marten project: diff --git a/docs/versioned_docs/version-0.2/models-and-databases.mdx b/docs/versioned_docs/version-0.5/models-and-databases.mdx similarity index 87% rename from docs/versioned_docs/version-0.2/models-and-databases.mdx rename to docs/versioned_docs/version-0.5/models-and-databases.mdx index 31375a1e8..bb09bfb2c 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases.mdx +++ b/docs/versioned_docs/version-0.5/models-and-databases.mdx @@ -15,6 +15,9 @@ Models define what data can be persisted and manipulated by a Marten application
+
+ +
@@ -49,6 +52,9 @@ Models define what data can be persisted and manipulated by a Marten application
+
+ +
diff --git a/docs/versioned_docs/version-0.2/models-and-databases/callbacks.md b/docs/versioned_docs/version-0.5/models-and-databases/callbacks.md similarity index 97% rename from docs/versioned_docs/version-0.2/models-and-databases/callbacks.md rename to docs/versioned_docs/version-0.5/models-and-databases/callbacks.md index 1c86b3eed..690a6be14 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/callbacks.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/callbacks.md @@ -4,9 +4,7 @@ description: Learn how to define model callbacks. sidebar_label: Callbacks --- -Models callbacks let you define logic that is triggered before or after a record's state alteration. They are methods that get called at specific stages of a record's lifecycle. For example, callbacks can be called when model instances are created, updated, or deleted. - -This documents covers the available callbacks and introduces you to the associated API, which you can use to define hooks in your models. +Models callbacks let you define logic that is triggered before or after a record's state alteration. They are methods that get called at specific stages of a record's lifecycle. For example, callbacks can be called when model instances are created, updated, or deleted. This documents covers the available callbacks and introduces you to the associated API, which you can use to define hooks in your models. ## Overview diff --git a/docs/versioned_docs/version-0.2/models-and-databases/how-to/create-custom-model-fields.md b/docs/versioned_docs/version-0.5/models-and-databases/how-to/create-custom-model-fields.md similarity index 95% rename from docs/versioned_docs/version-0.2/models-and-databases/how-to/create-custom-model-fields.md rename to docs/versioned_docs/version-0.5/models-and-databases/how-to/create-custom-model-fields.md index 7aa272d35..d1596e4bb 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/how-to/create-custom-model-fields.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/how-to/create-custom-model-fields.md @@ -22,7 +22,7 @@ Creating a custom model field does not necessarily mean that all of these respon Regardless of the approach you take in order to define new model field classes ([subclassing built-in fields](#subclassing-existing-model-fields), or [creating new ones from scratch](#creating-new-model-fields-from-scratch)), these classes must be registered to the Marten's global fields registry in order to make them available for use when defining models. -To do so, you will have to call the [`Marten::DB::Field#register`](pathname:///api/0.2/Marten/DB/Field.html#register(id%2Cfield_klass)-macro) method with the identifier of the field you wish to use, and the actual field class. For example: +To do so, you will have to call the [`Marten::DB::Field#register`](pathname:///api/0.5/Marten/DB/Field.html#register(id%2Cfield_klass)-macro) method with the identifier of the field you wish to use, and the actual field class. For example: ```crystal Marten::DB::Field.register(:foo, FooField) @@ -44,7 +44,7 @@ The call to `#register` can be made from anywhere in your codebase, but obviousl The easiest way to introduce a model field is probably to subclass one of the [built-in model fields](../reference/fields.md) provided by Marten. This can make a lot of sense if the "type" of the field you are trying to implement is already supported by Marten. -For example, implementing a custom "email" field could be done by subclassing the existing [`Marten::DB::Field::String`](pathname:///api/0.2/Marten/DB/Field/String.html) class. Indeed, an "email" field is essentially a string with a pre-defined maximum size and some additional validation logic: +For example, implementing a custom "email" field could be done by subclassing the existing [`Marten::DB::Field::String`](pathname:///api/0.5/Marten/DB/Field/String.html) class. Indeed, an "email" field is essentially a string with a pre-defined maximum size and some additional validation logic: ```crystal class EmailField < Marten::DB::Field::String @@ -82,7 +82,7 @@ Everything that is described in the following section about [creating model fiel ## Creating new model fields from scratch -Creating new model fields from scratch involves subclassing the [`Marten::DB::Field::Base`](pathname:///api/0.2/Marten/DB/Field/Base.html) abstract class. Because of this, the new field class is required to implement a set of mandatory methods. These mandatory methods, and some other ones that are optional (but interesting in terms of capabilities), are described in the following sections. +Creating new model fields from scratch involves subclassing the [`Marten::DB::Field::Base`](pathname:///api/0.5/Marten/DB/Field/Base.html) abstract class. Because of this, the new field class is required to implement a set of mandatory methods. These mandatory methods, and some other ones that are optional (but interesting in terms of capabilities), are described in the following sections. ### Mandatory methods @@ -137,11 +137,11 @@ def from_db_result_set(result_set : ::DB::ResultSet) : ::UUID? end ``` -The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#fromdb) once you get the value from the database result set in order to return the final value. +The `#from_db_result_set` method is supposed to return the read value into the right "representation", that is the final object representing the field value that users will interact with when manipulating model records (for example a `UUID` object created from a string). As such, you will usually want to call [`#from_db`](#from_db) once you get the value from the database result set in order to return the final value. #### `to_column` -Most model fields will contribute a corresponding column at the database level; these columns are read by Marten in order to generate migrations from model definitions. The column returned by the `#to_column` method should be an instance of a subclass of [`Marten::DB::Management::Column::Base`](pathname:///api/0.2/Marten/DB/Management/Column/Base.html). +Most model fields will contribute a corresponding column at the database level; these columns are read by Marten in order to generate migrations from model definitions. The column returned by the `#to_column` method should be an instance of a subclass of [`Marten::DB::Management::Column::Base`](pathname:///api/0.5/Marten/DB/Management/Column/Base.html). For example, an "email" field could return a string column as part of its `#to_column` method: @@ -163,7 +163,7 @@ If for some reason your custom field does not contribute any columns to the data #### `to_db` -The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#fromdb) method. +The `#to_db` method converts a field value from the "Crystal" representation to the database representation. As such, this method performs the reverse operation of the [`#from_db`](#from_db) method. For example, this method could return the string representation of a `UUID` object: @@ -186,7 +186,7 @@ Again, if the value can't be processed properly by the field class, it may be ne #### `initialize` -The default `#initialize` method that is provided by the [`Marten::DB::Field::Base`](pathname:///api/0.2/Marten/DB/Field/Base.html) is fairly simply and looks like this: +The default `#initialize` method that is provided by the [`Marten::DB::Field::Base`](pathname:///api/0.5/Marten/DB/Field/Base.html) is fairly simply and looks like this: ```crystal def initialize( diff --git a/docs/versioned_docs/version-0.2/models-and-databases/introduction.md b/docs/versioned_docs/version-0.5/models-and-databases/introduction.md similarity index 83% rename from docs/versioned_docs/version-0.2/models-and-databases/introduction.md rename to docs/versioned_docs/version-0.5/models-and-databases/introduction.md index bebe943ec..0a305ece5 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/introduction.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/introduction.md @@ -8,7 +8,7 @@ Models define what data can be persisted and manipulated by a Marten application ## Basic model definition -Marten models must be defined as subclasses of the [`Marten::Model`](pathname:///api/0.2/Marten/DB/Model.html) base class; they explicitly define "fields" through the use of the `field` macro. These classes and fields map to database tables and columns that can be queried through the use of an automatically-generated database access API (see [Queries](./queries.md) for more details). +Marten models must be defined as subclasses of the [`Marten::Model`](pathname:///api/0.5/Marten/DB/Model.html) base class; they explicitly define "fields" through the use of the `field` macro. These classes and fields map to database tables and columns that can be queried through the use of an automatically-generated database access API (see [Queries](./queries.md) for more details). For example, the following code snippet defines a simple `Article` model: @@ -134,28 +134,11 @@ end Marten provides special fields allowing to define the three most common types of database relationships: many-to-many, many-to-one, and one-to-one. -#### Many-to-many relationships - -Many-to-many relationships can be defined through the use of [`many_to_many`](./reference/fields.md#many_to_many) fields. This special field type requires the use of a special `to` argument in order to specify the model class to which the current model is related. - -For example, an `Article` model could have a many-to-many field towards a `Tag` model. In such case, an `Article` record could have many associated `Tag` records, and every `Tag` record could be associated to many `Article` records as well: - -```crystal -class Tag < Marten::Model - # ... -end - -class Article < Marten::Model - # ... - field :tags, :many_to_many, to: Tag -end -``` - #### Many-to-one relationships Many-to-one relationships can be defined through the use of [`many_to_one`](./reference/fields.md#many_to_one) fields. This special field type requires the use of a special `to` argument in order to specify the model class to which the current model is related. -For example, an `Article` model could have a many-to-one field towards an `Author` model. In such case, an `Article` record would only have one associated `Author` record, but every `Author` record could be associated to many `Article` records: +For example, an `Article` model could have a many-to-one field towards an `Author` model. In such case, an `Article` record would only have one associated `Author` record, but every `Author` record could be associated with many `Article` records: ```crystal class Author < Marten::Model @@ -179,6 +162,10 @@ end ``` ::: +:::info +Please refer to [Many-to-one relationships](./relationships.md#many-to-one-relationships) to learn more about this type of model relationship. +::: + #### One-to-one relationships One-to-one relationships can be defined through the use of [`one_to_one`](./reference/fields.md#one_to_one) fields. This special field type requires the use of a special `to` argument in order to specify the model class to which the current model is related. @@ -196,9 +183,34 @@ class User < Marten::Model end ``` +:::info +Please refer to [One-to-one relationships](./relationships.md#one-to-one-relationships) to learn more about this type of model relationship. +::: + +#### Many-to-many relationships + +Many-to-many relationships can be defined through the use of [`many_to_many`](./reference/fields.md#many_to_many) fields. This special field type requires the use of a special `to` argument in order to specify the model class to which the current model is related. + +For example, an `Article` model could have a many-to-many field towards a `Tag` model. In such case, an `Article` record could have many associated `Tag` records, and every `Tag` record could be associated with many `Article` records as well: + +```crystal +class Tag < Marten::Model + # ... +end + +class Article < Marten::Model + # ... + field :tags, :many_to_many, to: Tag +end +``` + +:::info +Please refer to [Many-to-many relationships](./relationships.md#many-to-many-relationships) to learn more about this type of model relationship. +::: + ### Timestamps -Marten lets you easily add automatic `created_at` / `updated_at` [`date_time`](./reference/fields.md#date_time) fields to your models by leveraging the [`#with_timestamp_fields`](pathname:///api/0.2/Marten/DB/Model/Table.html#with_timestamp_fields-macro) macro: +Marten lets you easily add automatic `created_at` / `updated_at` [`date_time`](./reference/fields.md#date_time) fields to your models by leveraging the [`#with_timestamp_fields`](pathname:///api/0.5/Marten/DB/Model/Table.html#with_timestamp_fields-macro) macro: ```crystal class Article < Marten::Model @@ -212,7 +224,7 @@ end The `created_at` field is populated with the current time when new records are created while the `updated_at` field is refreshed with the current time whenever records are updated. -Note that using [`#with_timestamp_fields`](pathname:///api/0.2/Marten/DB/Model/Table.html#with_timestamp_fields-macro) is technically equivalent as defining two `created_at` and `updated_at` [`date_time`](./reference/fields.md#date_time) fields as follows: +Note that using [`#with_timestamp_fields`](pathname:///api/0.5/Marten/DB/Model/Table.html#with_timestamp_fields-macro) is technically equivalent as defining two `created_at` and `updated_at` [`date_time`](./reference/fields.md#date_time) fields as follows: ```crystal class Article < Marten::Model @@ -232,7 +244,7 @@ Single model fields can be indexed or associated with a unique constraint _indiv ### Multifields indexes -Multifields indexes can be configured in a model by leveraging the [`#db_index`](pathname:///api/0.2/Marten/DB/Model/Table/ClassMethods.html#db_index(name%3AString|Symbol%2Cfield_names%3AArray(String)|Array(Symbol))%3ANil-instance-method) class method. This method requires an index name argument as well as an array of targeted field names. +Multifields indexes can be configured in a model by leveraging the [`#db_index`](pathname:///api/0.5/Marten/DB/Model/Table/ClassMethods.html#db_index(name%3AString|Symbol%2Cfield_names%3AArray(String)|Array(Symbol))%3ANil-instance-method) class method. This method requires an index name argument as well as an array of targeted field names. For example: @@ -248,7 +260,7 @@ end ### Multifields unique constraints -Multifields unique constraints can be configured in a model by leveraging the [`#db_unique_constraint`](pathname:///api/0.2/Marten/DB/Model/Table/ClassMethods.html#db_unique_constraint(name%3AString|Symbol%2Cfield_names%3AArray(String)|Array(Symbol))%3ANil-instance-method) class method. This method requires an index name argument as well as an array of targeted field names. +Multifields unique constraints can be configured in a model by leveraging the [`#db_unique_constraint`](pathname:///api/0.5/Marten/DB/Model/Table/ClassMethods.html#db_unique_constraint(name%3AString|Symbol%2Cfield_names%3AArray(String)|Array(Symbol))%3ANil-instance-method) class method. This method requires an index name argument as well as an array of targeted field names. For example: @@ -393,13 +405,9 @@ Please head over to the [Model validations](./validations.md) guide in order to Model classes can inherit from each other. This allows you to easily reuse the field definitions and table attributes of a parent model within a child model. -Presently, the Marten web framework only allows [abstract model inheritance](#inheriting-from-abstract-models) (which is useful in order to reuse shared model fields and patterns over multiple child models without having a database table created for the parent model). Support for multi-table inheritance is planned for future releases. +Presently, the Marten web framework allows [abstract model inheritance](#abstract-model-inheritance) (which is useful in order to reuse shared model fields and patterns over multiple child models without having a database table created for the parent model) and [multi-table inheritance](#multi-table-inheritance). -:::caution -You can technically inherit from concrete model classes, but this will result in the same behavior as the [abstract model technique](#inheriting-from-abstract-models). As mentioned previously, this behavior is likely to change in future Marten versions and you should probably not rely on it. -::: - -### Inheriting from abstract models +### Abstract model inheritance You can define abstract model classes by leveraging [Crystal's abstract type mechanism](https://crystal-lang.org/reference/syntax_and_semantics/virtual_and_abstract_types.html). Doing so allows to easily reuse model field definitions, table properties, and custom logics within child models. In this situation, the parent's model does not contribute any table to the considered database. @@ -419,6 +427,54 @@ end The `Student` model will have four model fields in total (`id`, `name`, `email`, and `grade`). Moreover, all the methods of the parent model fields will be available on the child model. It should be noted that in this case the `Person` model cannot be used like a regular model: for example, trying to query records will return an error since no table is actually associated with the abstract model. Since it is an [abstract type](https://crystal-lang.org/reference/syntax_and_semantics/virtual_and_abstract_types.html), the `Student` class can't be instantiated either. +### Multi table inheritance + +Marten also supports another form of model inheritance, where each model in the hierarchy is a concrete model (i.e., a model that is not abstract). In this situation, each model can be used/queried individually and has its own associated database. The framework upholds "links" between each model that uses multi table inheritance and its parent models in order to ensure that the relational structure and inheritance hierarchy are accurately maintained. + +For example, let's consider the following models: + +```crystal +class Person < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :first_name, :string, max_size: 100 + field :last_name, :string, max_size: 100 +end + +class Employee < Person + field :company_name, :string, max_size: 100 +end +``` + +All the fields defined in the `Person` model will be accessible when interacting with records of the `Employee` model, despite the fact that the data itself is stored in distinct tables. This means that it will be possible to filter `Employee` records using fields defined in the `Person` model, and to interact with the corresponding attributes when manipulating the obtained model instances: + +```crystal +employee = Employee.filter(first_name: "John").first! +employee.first_name # => "John" +``` + +Initializing or creating `Employee` records will also work as you would expect if all the fields were defined in the `Employee` model class: + +```crystal +employee = Employee.create!( + first_name: "John", + last_name: "Doe", + company_name: "Super org" +) +``` + +Additionaly, it's important to note that attempting to filter or retrieve `Person` records will return `Person` instances. When manipulating a parent model instance, it is possible to get a child model record by calling the `#` method - with `child_model` being the downcased version of the child model name. For example: + +```crystal +person = Person.filter(first_name: "John").first! +person.employee # => # +``` + +You should note that if the `Person` record is not an employee, then calling `#employee` will return `nil`. + +:::note +It's important to note that when retrieving and filtering model records that utilize multi-table inheritance (such as child model records), there will be added join operations within the underlying SQL queries. These joins are necessary to assemble the complete data from various related tables, potentially affecting the overall query performance. +::: + ## Callbacks It is possible to define callbacks in your model in order to bind methods and logics to specific events in the life cycle of your model records. For example, it is possible to define callbacks that run before a record gets created, or before it is destroyed. diff --git a/docs/versioned_docs/version-0.2/models-and-databases/migrations.md b/docs/versioned_docs/version-0.5/models-and-databases/migrations.md similarity index 98% rename from docs/versioned_docs/version-0.2/models-and-databases/migrations.md rename to docs/versioned_docs/version-0.5/models-and-databases/migrations.md index 2e86bde83..77919d933 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/migrations.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/migrations.md @@ -86,7 +86,7 @@ Finally, it should be noted that SQLite does not support most schema alteration As presented in the [overview](#overview) section above, migration files are automatically generated by Marten by identifying changes in your model definitions (although migrations could be created and defined manually if needed). These files are persisted in a `migrations` folder inside each application's main directory. -Migrations always inherit from the [`Marten::Migration`](pathname:///api/0.2/Marten/DB/Migration.html) base class. A basic migration will look something like this: +Migrations always inherit from the [`Marten::Migration`](pathname:///api/0.5/Marten/DB/Migration.html) base class. A basic migration will look something like this: ```crystal # Generated by Marten 0.1.0 on 2022-03-13 16:08:37 -04:00 @@ -105,7 +105,7 @@ Each migration must define the following mandatory information: the migration's ### Dependencies -Marten migrations can depend upon one another. As such, each migration generally defines one or many dependencies through the use of the [`#depends_on`](pathname:///api/0.2/Marten/DB/Migration.html#depends_on(app_name%3AString|Symbol%2Cmigration_name%3AString|Symbol)-class-method) class method, which takes an application label and a migration name as positional arguments. +Marten migrations can depend upon one another. As such, each migration generally defines one or many dependencies through the use of the [`#depends_on`](pathname:///api/0.5/Marten/DB/Migration.html#depends_on(app_name%3AString|Symbol%2Cmigration_name%3AString|Symbol)-class-method) class method, which takes an application label and a migration name as positional arguments. Migration dependencies are used to ensure that changes to a database are applied in the right order. For example, the previous migration is part of the `main` app and depends on two other migrations: first, it depends on the previous migration of the `main` app (`202203131604261_add_content_to_main_article_table`). Secondly, it depends on the `202203131607261_create_users_user_table` migration of the `users` app. This makes sense considering that the migration's only operation is adding an `author_id` foreign key targetting the `users_user` table to the `main_article` table: the dependency instructs Marten to first apply the `202203131607261_create_users_user_table` migration (which creates the `users_user` table) before applying the migration adding the new foreign key (since this foreign key requires the targetted table to exist first in order to be created properly). @@ -205,7 +205,7 @@ $ marten migrate my_app 202203111821451 --fake The operations of a migrations will be executed inside a single transaction by default unless this capability is not supported by the database backend (which is the case for MySQL). -It is possible to disable this default behavior by using the [`#atomic`](pathname:///api/0.2/Marten/DB/Migration.html#atomic(atomic%3ABool)-class-method) method, in the migration class: +It is possible to disable this default behavior by using the [`#atomic`](pathname:///api/0.5/Marten/DB/Migration.html#atomic(atomic%3ABool)-class-method) method, in the migration class: ```crystal # Generated by Marten 0.1.0 on 2022-03-30 22:13:06 -04:00 @@ -327,7 +327,7 @@ Generating migrations for app 'my_app': ○ Create my_app_article table ``` -If we look at the generated migration, you will notice that it includes multiple calls to the [`#replaces`](pathname:///api/0.2/Marten/DB/Migration.html#replaces(app_name%3AString|Symbol%2Cmigration_name%3AString|Symbol)-class-method) class method: +If we look at the generated migration, you will notice that it includes multiple calls to the [`#replaces`](pathname:///api/0.5/Marten/DB/Migration.html#replaces(app_name%3AString|Symbol%2Cmigration_name%3AString|Symbol)-class-method) class method: ```crystal # Generated by Marten 0.1.0 on 2022-06-26 16:46:06 -04:00 diff --git a/docs/versioned_docs/version-0.2/models-and-databases/multiple-databases.md b/docs/versioned_docs/version-0.5/models-and-databases/multiple-databases.md similarity index 95% rename from docs/versioned_docs/version-0.2/models-and-databases/multiple-databases.md rename to docs/versioned_docs/version-0.5/models-and-databases/multiple-databases.md index 913ba4169..827d5e016 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/multiple-databases.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/multiple-databases.md @@ -11,7 +11,7 @@ Support for multi-database projects is still experimental and lacking features s ## Defining multiple databases -Each Marten project leveraging a single database uses what is called a "default" database. This is the database whose configuration is defined when calling the [`#database`](pathname:///api/0.2/Marten/Conf/GlobalSettings.html#database(id%3DDB%3A%3AConnection%3A%3ADEFAULT_CONNECTION_NAME%2C%26)-instance-method) configuration method: +Each Marten project leveraging a single database uses what is called a "default" database. This is the database whose configuration is defined when calling the [`#database`](pathname:///api/0.5/Marten/Conf/GlobalSettings.html#database(id%3DDB%3A%3AConnection%3A%3ADEFAULT_CONNECTION_NAME%2C%26)-instance-method) configuration method: ```crystal config.database do |db| @@ -22,7 +22,7 @@ end The "default" database is implied whenever you interact with the database (eg. by performing queries, creating records, etc), unless specified otherwise. -The [`#database`](pathname:///api/0.2/Marten/Conf/GlobalSettings.html#database(id%3DDB%3A%3AConnection%3A%3ADEFAULT_CONNECTION_NAME%2C%26)-instance-method) configuration method can take an additional argument in order to define additional databases. For example: +The [`#database`](pathname:///api/0.5/Marten/Conf/GlobalSettings.html#database(id%3DDB%3A%3AConnection%3A%3ADEFAULT_CONNECTION_NAME%2C%26)-instance-method) configuration method can take an additional argument in order to define additional databases. For example: ```crystal config.database :other_db do |db| diff --git a/docs/versioned_docs/version-0.2/models-and-databases/queries.md b/docs/versioned_docs/version-0.5/models-and-databases/queries.md similarity index 64% rename from docs/versioned_docs/version-0.2/models-and-databases/queries.md rename to docs/versioned_docs/version-0.5/models-and-databases/queries.md index 4b5772141..1ea701629 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/queries.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/queries.md @@ -126,11 +126,19 @@ By default, filters involving multiple parameters like in the above examples alw ```crystal # Get Author records with either "Bob" or "Alice" as first name Author.filter { q(first_name: "Bob") | q(first_name: "Alice") } - + # Get Author records whose first names are not "John" Author.filter { -q(first_name: "Alice") } ``` +Marten also has the option to filter query sets using [raw SQL predicates](./raw-sql#filtering-with-raw-sql-predicates). This is useful when you want to leverage the flexibility of SQL for specific conditions, but still want Marten to handle the column selection and query building for the rest of the query. To use raw SQL predicates, can specify a string containing the predicate with optional parameters to the `#filter` query set method: + +```crystal +Author.filter("first_name = :first_name", first_name: "John") +Author.filter("first_name = ?", "John") +Author.filter { q("first_name = :first_name", first_name: "John") } +``` + ### Excluding specific records Excluding records is achieved through the use of the `#exclude` method. This method provides exactly the same API as the [`#filter`](#filtering-specific-records) method outlined previously. It requires one or many predicate keyword arguments (in the format described in [Field predicates](#field-predicates)). For example: @@ -252,7 +260,7 @@ Article.filter { q(title__startswith: "Top") | q(title__startswith: "10") } Using this approach, it is possible to produce complex conditions by combining `q()` expressions with the `&`, `|`, and `-` operators. Parentheses can also be used to group statements: ```crystal -Article.filter { +Article.filter { (q(title__startswith: "Top") | q(title__startswith: "10")) & -q(author__first_name: "John") } ``` @@ -260,12 +268,12 @@ Article.filter { Finally it should be noted that you can define many field predicates _inside_ `q()` expressions. When doing so, the field predicates will be "AND"ed together: ```crystal -Article.filter { +Article.filter { q(title__startswith: "Top") & -q(author__first_name: "John", author__last_name: "Doe") } ``` -### Joins and filtering relations +### Filtering relations The double underscores notation described previously (`__`) can also be used to filter based on related model fields. For example, in the considered models definitions, we have an `Article` model which defines a relation (`many_to_one` field) to the `Author` model through the `author` field. The `Author` model itself also defines a relation to a `City` record through the `hometown` field. @@ -287,14 +295,22 @@ And obviously, the above query sets could also be used along with more specific Author.filter(author__hometown__name__startswith: "New") ``` -When doing “deep filtering” like this, related model tables are automatically "joined" at the SQL level (inner joins or left outer joins depending on the nullability of the filtered fields). So the filtered relations are also already "selected" as part of the query, and fully initialized at the Crystal level. +When doing “deep filtering” like this, related model tables are automatically "joined" at the SQL level to perform the query (inner joins or left outer joins are used depending on the nullability of the filtered fields). -It is also possible to explicitly define that a specific query set must "join" a set of relations. This can result in nice performance improvements since this can help reduce the number of SQL queries performed for a given codebase. This is achieved through the use of the `#join` method: +It is worth noting that this filtering capability also works for [many-to-many relationships](./relationships.md#many-to-many-relationships) and reverse relations. For example, assuming that the `Article` model defines a `tags` [many-to-many](./reference/fields.md#many_to_many) field towards a hypothetical `Tag` model, the following query would be possible: + +```crystal +Article.filter(tags__label: "crystal") +``` + +### Pre-selecting relations with joins + +It is also possible to explicitly define that a specific query set must "join" a set of relations. This can result in nice performance improvements since this can help reduce the number of SQL queries performed for a given codebase. This is achieved through the use of the [`#join`](./reference/query-set.md#join) method: ```crystal author_1 = Author.filter(first_name: "John") puts author_1.hometown # DB hit to retrieve the associated City record - + author_2 = Author.join(:hometown).filter(first_name: "John") puts author_2.hometown # No additional DB hit ``` @@ -302,14 +318,59 @@ puts author_2.hometown # No additional DB hit The double underscores notations can also be used in the context of joins. For example: ```crystal -# The associated Author and City records will be selected and fully initialized +# The associated Author and City records will be selected and fully initialized # with the selected Article record. Article.join(:author__hometown).get(id: 42) ``` +Finally, it is worth mentioning that many relations can be specified to [`#join`](./reference/query-set.md#join). For example: + +```crystal +Article.join(:author__hometown, :edited_by) +``` + +:::info +Please note that the [`#join`](./reference/query-set.md#join) query set method can only be used on [many-to-one](./relationships.md#many-to-one-relationships) relationships, [one-to-one](./relationships.md#one-to-one-relationships) relationships, and reverse one-to-one relations. For multi-valued relations, please consider [pre-fetching records](#pre-fetching-relations). +::: + +### Pre-fetching relations + +While [pre-selecting relations with joins](#pre-selecting-relations-with-joins) can result in performance improvements (and help in reducing the number of SQL queries) by performing joins at the SQL level, is also possible to _pre-fetch relations_ using the [`#prefetch`](./reference/query-set.md#prefetch) method. + +Both methods serve a common purpose, aiming to alleviate N+1 issues commonly encountered when accessing related objects. However, their strategies diverge in approach: + +* When using [`#join`](./reference/query-set.md#join), the specified relationships are followed and each record returned by the considered query set has the corresponding related objects already selected and populated. The performance improvements are achieved by reducing the number of SQL queries since related records are retrieved by creating an SQL join and by including their fields in the main SELECT statement. Because of this, [`#join`](./reference/query-set.md#join) can only be used on single-valued relationships: [many-to-one](./relationships.md#many-to-one-relationships) relationships, [one-to-one](./relationships.md#one-to-one-relationships) relationships, and reverse one-to-one relations. +* When using [`#prefetch`](./reference/query-set.md#prefetch), the records corresponding to the specified relationships will be prefetched in single batches and each record returned by the original query set will have the corresponding related objects already selected and populated. As such, [`#prefetch`](./reference/query-set.md#prefetch) can be used with any kind of relationship: [many-to-one](./relationships.md#many-to-one-relationships) relationships, [one-to-one](./relationships.md#one-to-one-relationships) relationships, [many-to-many](./relationships.md#many-to-many-relationships) relationships, and all types of reverse relations. + +For example, assuming that a `Post` model defines a `tags` many-to-many field: + +```crystal +posts_1 = Post.all.to_a +# hits the database to retrieve the related "tags" (many-to-many relation) +puts posts_1[0].tags.to_a + +posts_2 = Post.all.prefetch(:tags).to_a +# doesn't hit the database since the related "tags" relation was already prefetched +puts posts_2[0].tags.to_a +``` + +The double underscores notations can also be used when pre-fetching relations. In this situation, the records targeted by the original query set will be decorated with the prefetched records, and those records will be decorated with the following prefetched records. For example: + +```crystal +# The associated Book and BookGenres records will be pre-fetched and fully initialized +# at the Author and Book records levels. +Author.prefetch(:books__genres) +``` + +Finally, it is worth mentioning that multiple relations can be specified to [`#prefetch`](./reference/query-set.md#prefetch). For example: + +```crystal +Author.prefetch(:books__genres, :publisher) +``` + ### Pagination -Marten provides a pagination mechanism that you can leverage in order to easily iterate over records that are split across several pages of data. This works as follows: each query set object lets you generate a "paginator" (instance of [`Marten::DB::Query::Paginator`](pathname:///api/0.2/Marten/DB/Query/Paginator.html)) from a given page size (the number of records you would like on each page). You can then use this paginator in order to request specific pages, which gives you access to the corresponding records and to some additional pagination metadata. +Marten provides a pagination mechanism that you can leverage in order to easily iterate over records that are split across several pages of data. This works as follows: each query set object lets you generate a "paginator" (instance of [`Marten::DB::Query::Paginator`](pathname:///api/0.5/Marten/DB/Query/Paginator.html)) from a given page size (the number of records you would like on each page). You can then use this paginator in order to request specific pages, which gives you access to the corresponding records and to some additional pagination metadata. For example: @@ -330,7 +391,7 @@ page.next_page? # => true page.next_page_number # => 2 ``` -As you can see, paginator objects let you request specific pages by providing a page number (1-indexed!) to the [`#page`](pathname:///api/0.2/Marten/DB/Query/Paginator.html#page(number%3AInt)-instance-method) method. Such pages are instances of [`Marten::DB::Query::Page`](pathname:///api/0.2/Marten/DB/Query/Page.html) and give you the ability to easily iterate over the corresponding records. They also give you the ability to retrieve some pagination-related information (eg. about the previous and next pages by leveraging the [`#previous_page?`](pathname:///api/0.2/Marten/DB/Query/Page.html#previous_page%3F-instance-method), [`#previous_page_number`](pathname:///api/0.2/Marten/DB/Query/Page.html#previous_page_number-instance-method), [`#next_page?`](pathname:///api/0.2/Marten/DB/Query/Page.html#next_page%3F-instance-method), and [`#next_page_number`](pathname:///api/0.2/Marten/DB/Query/Page.html#next_page_number-instance-method) methods). +As you can see, paginator objects let you request specific pages by providing a page number (1-indexed!) to the [`#page`](pathname:///api/0.5/Marten/DB/Query/Paginator.html#page(number%3AInt)-instance-method) method. Such pages are instances of [`Marten::DB::Query::Page`](pathname:///api/0.5/Marten/DB/Query/Page.html) and give you the ability to easily iterate over the corresponding records. They also give you the ability to retrieve some pagination-related information (eg. about the previous and next pages by leveraging the [`#previous_page?`](pathname:///api/0.5/Marten/DB/Query/Page.html#previous_page%3F-instance-method), [`#previous_page_number`](pathname:///api/0.5/Marten/DB/Query/Page.html#previous_page_number-instance-method), [`#next_page?`](pathname:///api/0.5/Marten/DB/Query/Page.html#next_page%3F-instance-method), and [`#next_page_number`](pathname:///api/0.5/Marten/DB/Query/Page.html#next_page_number-instance-method) methods). ## Updating records @@ -366,3 +427,118 @@ Article.filter(title: "My article").delete ``` By default, related objects that are associated with the deleted records will also be deleted by following the deletion strategy defined in each relation field (`on_delete` option, see the [reference](./reference/fields.md#on_delete) for more details). The method always returns the number of deleted records. + +## Scopes + +Scopes allow for the pre-definition of specific filtered query sets, which can be easily applied to model classes and model query sets. When defining such scopes, all the query set capabilities that were covered previously (such as [filtering records](#filtering-specific-records), [excluding records](#excluding-specific-records), etc) can be leveraged. + +### Defining scopes + +Scopes can be defined through the use of the [`#scope`](pathname:///api/0.5/Marten/DB/Model/Querying.html#scope(name%2C%26block)-macro) macro. This macro expects a scope name (string literal or symbol) as first argument and requires a block where the query set filtering logic is defined. + +For example: + +```crystal +class Post < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :is_published, :bool, default: false + field :created_at, :date_time + + // highlight-next-line + scope :published { filter(is_published: true) } + // highlight-next-line + scope :unpublished { filter(is_published: false) } + // highlight-next-line + scope :recent { filter(created_at__gt: 1.year.ago) } +end +``` + +Considering the above model definition, it is possible to get published posts by using the following method call: + +```crystal +Post.published # => Post::QuerySet [...]> +``` + +Similarly, retrieving all published posts from a query set object can be accomplished by calling the `#published` method on the query set object: + +```crystal +query_set = Post.all +query_set.published # => Post::QuerySet [...]> +``` + +Because of this capability, it is important to note that scopes can technically be chained. For example, the following snippet will return all the published posts that were created less than one year ago: + +```crystal +Post.published.recent # => Post::QuerySet [...]> +``` + +### Defining scopes with arguments + +If needed, you can define scopes that require arguments. To accomplish this, simply include the required arguments within the scope block. + +For example: + +```crystal +class Post < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :author, :many_to_one, to: Author + + // highlight-next-line + scope :by_author_id { |author_id| filter(author_id: author_id) } +end +``` + +Scopes that require arguments can be used in the same way as argument-free scopes; they can be called on model classes or model query sets: + +```crystal +Post.by_author_id(42) # => Post::QuerySet [...]> + +query_set = Post.all +query_set.by_author_id(42) # => Post::QuerySet [...]> +``` + +### Defining default scopes + +By default, querying all model records returns unfiltered query sets. However, you can define a default scope to automatically apply a specific filter to all queries for that model. This ensures that certain criteria are consistently enforced without the need to explicitly include a specific filter in every query. + +Default scopes can be defined through the use of the [`#default_scope`](pathname:///api/0.5/Marten/DB/Model/Querying.html#default_scope-macro) macro. This macro requires a block where the query set filtering logic is defined. + +For example: + +```crystal +class Post < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :is_published, :bool, default: false + field :created_at, :date_time + + // highlight-next-line + default_scope { filter(is_published: true) } +end +``` + +### Disabling scoping + +It is worth mentioning that unscoped model records are always accessible through the use of the [`#unscoped`](pathname:///api/0.5/Marten/DB/Model/Querying/ClassMethods.html#unscoped-instance-method) class method. This is especially useful if your model defines a default scope and you need to override it for certain queries. + +For example: + +```crystal +class Post < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :is_published, :bool, default: false + field :created_at, :date_time + + // highlight-next-line + default_scope { filter(is_published: true) } +end +``` + +Considering, the above model definition, you can retrieve all the `Post` records by bypassing the default scope with: + +```crystal +Post.unscoped # => Post::QuerySet [...]> +``` diff --git a/docs/versioned_docs/version-0.2/models-and-databases/raw-sql.md b/docs/versioned_docs/version-0.5/models-and-databases/raw-sql.md similarity index 67% rename from docs/versioned_docs/version-0.2/models-and-databases/raw-sql.md rename to docs/versioned_docs/version-0.5/models-and-databases/raw-sql.md index 35f1b7362..8223aacff 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/raw-sql.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/raw-sql.md @@ -19,7 +19,7 @@ end ``` :::tip -You need to know the name of the model table you are targetting to use the [`#raw`](./reference/query-set.md#raw) query set method. Unless you have explicitly overridden this name by using the [`#db_table`](pathname:///api/0.2/Marten/DB/Model/Table/ClassMethods.html#db_table(db_table%3AString|Symbol)-instance-method) class method, the name of the model table is automatically generated by Marten using the following format: `_` (`model_name` being the underscore version of the model class name). +You need to know the name of the model table you are targetting to use the [`#raw`](./reference/query-set.md#raw) query set method. Unless you have explicitly overridden this name by using the [`#db_table`](pathname:///api/0.5/Marten/DB/Model/Table/ClassMethods.html#db_table(db_table%3AString|Symbol)-instance-method) class method, the name of the model table is automatically generated by Marten using the following format: `_` (`model_name` being the underscore version of the model class name). ::: It should be noted that you can also "inject" parameters into your SQL query. To do so you have two options: either you specify these parameters as positional arguments, or you specify them as named arguments. Positional parameters must be specified using the `?` syntax while named parameters must be specified using the `:param` format. @@ -34,13 +34,13 @@ And the following one uses named parameters: ```crystal Article.raw( - "SELECT * FROM articles WHERE title = :title and created_at > :created_at", - title: "Hello World!", + "SELECT * FROM articles WHERE title = :title and created_at > :created_at", + title: "Hello World!", created_at: "2022-10-30" ) ``` -:::caution +:::caution **Do not use string interpolations in your SQL queries!** You should never use string interpolations in your raw SQL queries as this would expose your code to SQL injection attacks (where attackers can inject and execute arbitrary SQL into your database). @@ -62,6 +62,65 @@ Also, note that the parameters are left **unquoted** in the raw SQL queries: thi Finally, it should be noted that Marten does not validate the SQL queries you specify to the [`#raw`](./reference/query-set.md#raw) query set method. It is the developer's responsibility to ensure that these queries are (i) valid and (ii) that they return records that correspond to the considered model. +## Filtering with raw SQL predicates + +Marten provides a feature to filter query sets using raw SQL predicates within the `#filter` method. This is useful when you need more complex filtering logic than simple field comparisons but still want to leverage Marten's query building capabilities. + +Using raw SQL predicates involves specifying a string containing the actual predicate and optional parameters to the `#filter` query set method. For example: + +```crystal +Author.filter("author_id IS NOT NULL") +Author.filter("first_name = ?", "John") +``` + +### Specifying parameters + +You can "inject" parameters into your SQL raw predicates when using the `#filter` method. To do so you have two options: either you specify these parameters as positional arguments, or you specify them as named arguments. Positional parameters must be specified using the `?` syntax while named parameters must be specified using the `:param` format. + +For example, the following query uses positional parameters: + +```crystal +Article.filter("title = ? and created_at > ?", "Hello World!", "2022-10-30") +``` + +And the following one uses named parameters: + +```crystal +Article.filter( + "title = :title and created_at > :created_at", + title: "Hello World!", + created_at: "2022-10-30" +) +``` + +:::caution +**Do not use string interpolations in your SQL predicates!** + +You should never use string interpolations in your raw SQL predicates as this would expose your code to SQL injection attacks (where attackers can inject and execute arbitrary SQL into your database). + +As such, never - ever - do something like that: + +```crystal +Article.filter("title = '#{title}'") +``` + +And instead, do something like that: + +```crystal +Article.filter("title = ?", title) +``` + +Also, note that the parameters are left **unquoted** in the raw SQL queries: this is very important as not doing it would expose your code to SQL injection vulnerabilities as well. Parameters are quoted automatically by the underlying database backend. +::: + +### Using `q` expressions + +For even more flexibility, you can combine raw SQL predicates with the [`q` expression](./queries#complex-filters-with-q-expressions) syntax within a block: + +```crystal +Post.all.filter { q(category: "news") & q("created_at > ?", Time.local - 7.days) } +``` + ## Executing other SQL statements If it is necessary to execute other SQL statements that don't fall into the scope of what's provided by the [`#raw`](./reference/query-set.md#raw) query set method, then it's possible to rely on the low-level DB connection capabilities. @@ -77,7 +136,7 @@ end ``` :::tip -If you are using multiple databases and need to execute SQL statements on a database that is not the default one, then you can retrieve the considered DB connection object by using the [`Marten::DB::Connection#get`](pathname:///api/0.2/Marten/DB/Connection.html#get(db_alias%3AString|Symbol)-class-method) method. This method simply requires an argument corresponding to the DB alias you want to retrieve (ie. the alias you assigned to the database in the [databases configuration](../development/reference/settings.md#database-settings)) and returns the corresponding DB connection: +If you are using multiple databases and need to execute SQL statements on a database that is not the default one, then you can retrieve the considered DB connection object by using the [`Marten::DB::Connection#get`](pathname:///api/0.5/Marten/DB/Connection.html#get(db_alias%3AString|Symbol)-class-method) method. This method simply requires an argument corresponding to the DB alias you want to retrieve (ie. the alias you assigned to the database in the [databases configuration](../development/reference/settings.md#database-settings)) and returns the corresponding DB connection: ```crystal db = Marten::DB::Connection.get(:other_db) @@ -88,7 +147,7 @@ end ``` ::: -The [`#open`](pathname:///api/0.2/Marten/DB/Connection/Base.html#open(%26)-instance-method) method allows opening a connection to the considered database, which you can then use to perform queries. This method leverages Crystal's [DB opening mechanism](https://crystal-lang.org/reference/database/index.html#open-database) and it returns the same DB connection objects that you would get if you were using `DB#open` directly: +The [`#open`](pathname:///api/0.5/Marten/DB/Connection/Base.html#open(%26)-instance-method) method allows opening a connection to the considered database, which you can then use to perform queries. This method leverages Crystal's [DB opening mechanism](https://crystal-lang.org/reference/database/index.html#open-database) and it returns the same DB connection objects that you would get if you were using `DB#open` directly: ```crystal Marten::DB::Connection.default.open do |db| diff --git a/docs/versioned_docs/version-0.2/models-and-databases/reference/fields.md b/docs/versioned_docs/version-0.5/models-and-databases/reference/fields.md similarity index 74% rename from docs/versioned_docs/version-0.2/models-and-databases/reference/fields.md rename to docs/versioned_docs/version-0.5/models-and-databases/reference/fields.md index 9e4814f52..ffa8e101d 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/reference/fields.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/reference/fields.md @@ -84,13 +84,46 @@ The `auto_now` argument allows ensuring that the corresponding field value is au The `auto_now_add` argument allows ensuring that the corresponding field value is automatically set to the current time every time a record is created. This provides a convenient way to define `created_at` fields. Defaults to `false`. +### `duration` + +A `duration` field allows persisting duration values, which map to [`Time::Span`](https://crystal-lang.org/api/Time/Span.html) objects in Crystal. `duration` fields are persisted as big integer values (number of nanoseconds) at the database level. + ### `email` An `email` field allows to persist _valid_ email addresses. In addition to the [common field options](#common-field-options), such fields support the following arguments: #### `max_size` -The `max_size` argument is optional and defaults to 254 characters (in accordance with RFCs 3696 and 5321). It allows to specify the maximum size of the persisted email addresses. This maximum size is used for the corresponding column definition and when it comes to validate field values. +The `max_size` argument is optional and defaults to 254 characters (in accordance with RFCs 3696 and 5321). It allows to specify the maximum size of the persisted email addresses. This maximum size is used for the corresponding column definition and when it comes to validating field values. + +### `enum` + +An `enum` field allows persisting the value of an [`Enum`](https://crystal-lang.org/api/Enum.html). When defining `enum` fields, it's necessary to specify a `values` argument that matches the actual enum: + +```crystal +enum Category + NEWS + BLOG +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :category, :enum, values: Category +end + +article = Article.last! +article.category # => Category::BLOG +``` + +:::info +The way enums are handled at the database level depends on the database backend being used. Indeed, an ENUM type is used for MySQL databases while column checks are used for SQLite and PostgreSQL databases. +::: + +In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `values` + +The `values` argument **is required** and allows to specify the actual enum class that should be used for the field. It is worth mentioning that the configured enum will impact the values allowed for the corresponding column at the database level. ### `file` @@ -109,7 +142,7 @@ my_storage = Marten::Core::Storage::FileSystem.new(root: "files", base_url: "/fi class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, storage: my_storage + field :uploaded_file, :file, storage: my_storage end ``` @@ -124,7 +157,7 @@ If set to a string, it allows to define in which directory of the underlying sto ```crystal class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, upload_to: "foo/bar" + field :uploaded_file, :file, upload_to: "foo/bar" end ``` @@ -133,7 +166,7 @@ If set to a proc, it allows to customize the logic allowing to generate the resu ```crystal class Attachment < Marten::Model field :id, :big_int, primary_key: true, auto: true - field :file, :file, upload_to: ->(filename : String) { File.join("files/uploads", filename) } + field :uploaded_file, :file, upload_to: ->(filename : String) { File.join("files/uploads", filename) } end ``` @@ -158,6 +191,63 @@ class MyModel < Marten::Model end ``` +### `json` + +A `json` field allows persisting JSON values to the database. + +JSON values are automatically parsed from the underlying database column and exposed as a [`JSON::Any`](https://crystal-lang.org/api/JSON/Any.html) object (or `nil` if no values are available) by default in Crystal: + +```crystal +class MyModel < Marten::Model + # Other fields... + field :metadata, :json +end + +MyModel.last!.metadata # => JSON::Any object +``` + +Additionally, it is also possible to specify a [`serializable`](#serializable) option in order to specify a class that makes use of [`JSON::Serializable`](https://crystal-lang.org/api/JSON/Serializable.html). When doing so, the parsing of the JSON values will result in the initialization of the corresponding serializable objects: + +```crystal +class MySerializable + include JSON::Serializable + + property a : Int32 | Nil + property b : String | Nil +end + +class MyModel < Marten::Model + # Other fields... + field :metadata, :json, serializable: MySerializable +end + +MyModel.last!.metadata # => MySerializable object +``` + +:::info +It should be noted that `json` fields are mapped to: + +* `jsonb` columns in PostgreSQL databases +* `text` columns in MySQL databases +* `text` columns in SQLite databases +::: + +#### `serializable` + +The `serializable` arguments allows to specify that a class making use of [`JSON::Serializable`](https://crystal-lang.org/api/JSON/Serializable.html) should be used in order to parse the JSON values for the model field at hand. When specifying a `serializable` class, the values returned for the considered model fields will be instances of that class instead of [`JSON::Any`](https://crystal-lang.org/api/JSON/Any.html) objects. + +### `slug` + +A `slug` field allows to persist _valid_ slug values (ie. strings that can only include characters, numbers, dashes, and underscores). In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `max_size` + +The `max_size` argument is optional and defaults to 50 characters. It allows to specify the maximum size of the persisted email addresses. This maximum size is used for the corresponding column definition and when it comes to validate field values. + +:::info +As slug fields are usually used to query records, they are indexed by default. You can use the [`index`](#index) option (`index: false`) to disable auto-indexing. +::: + ### `string` A `string` field allows to persist small or medium string values. In addition to the [common field options](#common-field-options), such fields support the following arguments: @@ -166,6 +256,10 @@ A `string` field allows to persist small or medium string values. In addition to The `max_size` argument **is required** and allows to specify the maximum size of the persisted string. This maximum size is used for the corresponding column definition and when it comes to validate field values. +#### `min_size` + +The `min_size` argument allows defining the minimum size allowed for the persisted string. The default value for this argument is `nil`, which means that the minimum size is not validated by default. + ### `text` A `text` field allows to persist large text values. In addition to the [common field options](#common-field-options), such fields support the following arguments: @@ -174,6 +268,14 @@ A `text` field allows to persist large text values. In addition to the [common f The `max_size` argument allows to specify the maximum size of the persisted string. This maximum size is used when it comes to validate field values. Defaults to `nil`. +### `url` + +A `url` field allows persisting _valid_ URL addresses. In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `max_size` + +The `max_size` argument is optional and defaults to 200 characters. It allows to specify the maximum size of the persisted URLs. This maximum size is used for the corresponding column definition and when it comes to validate field values. + ### `uuid` A `uuid` field allows persisting Universally Unique IDentifiers (`UUID` objects). diff --git a/docs/versioned_docs/version-0.2/models-and-databases/reference/migration-operations.md b/docs/versioned_docs/version-0.5/models-and-databases/reference/migration-operations.md similarity index 100% rename from docs/versioned_docs/version-0.2/models-and-databases/reference/migration-operations.md rename to docs/versioned_docs/version-0.5/models-and-databases/reference/migration-operations.md diff --git a/docs/versioned_docs/version-0.2/models-and-databases/reference/query-set.md b/docs/versioned_docs/version-0.5/models-and-databases/reference/query-set.md similarity index 64% rename from docs/versioned_docs/version-0.2/models-and-databases/reference/query-set.md rename to docs/versioned_docs/version-0.5/models-and-databases/reference/query-set.md index 46d194b91..7c6e0edb1 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/reference/query-set.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/reference/query-set.md @@ -71,6 +71,36 @@ qset_2 = Article.all qset_2[2..6]? # returns a "sliced" query set ``` +### `&` (AND) + +Combines the current query set with another one using the **AND** operator. + +This method returns a new query set that is the result of combining the current query set with another one using the AND SQL operator. + +For example: + +```crystal +query_set_1 = Post.all.filter(title: "Test") +query_set_2 = Post.all.filter(is_published: true) + +combined_query_set = query_set_1 & query_set_2 +``` + +### `|` (OR) + +Combines the current query set with another one using the **OR** operator. + +This method returns a new query set that is the result of combining the current query set with another one using the OR SQL operator. + +For example: + +```crystal +query_set_1 = Post.all.filter(title: "Test") +query_set_2 = Post.all.filter(is_published: true) + +combined_query_set = query_set_1 | query_set_2 +``` + ### `all` Allows retrieving all the records of a specific model. `#all` can be used as a class method from any model class, or it can be used as an instance method from any query set object. In this last case, calling `#all` returns a copy of the current query set. @@ -93,7 +123,7 @@ query_set_1 = Post.all.distinct query_set_2 = Post.all.distinct(:title) ``` -It should be noted that it is also possible to follow associations of direct related models too by using the [double underscores notation](../queries.md#joins-and-filtering-relations) (`__`). For example the following query will select distinct records based on a joined "author" attribute: +It should be noted that it is also possible to follow associations of direct related models too by using the [double underscores notation](../queries.md#filtering-relations) (`__`). For example the following query will select distinct records based on a joined "author" attribute: ``` query_set = Post.all.distinct(:author__name) @@ -141,9 +171,9 @@ query_set.filter { (q(name: "Foo") | q(name: "Bar")) & q(is_published: True) } ### `join` -Returns a queryset whose specified `relations` are "followed" and joined to each result (see [Queries](../queries.md#joins-and-filtering-relations) for an introduction about this capability). +Returns a queryset whose specified `relations` are "followed" and joined to each result (see [Queries](../queries.md#filtering-relations) for an introduction about this capability). -When using `#join`, the specified foreign-key relationships will be followed and each record returned by the queryset will have the corresponding related objects already selected and populated. Using `#join` can result in performance improvements since it can help reduce the number of SQL queries, as illustrated by the following example: +When using `#join`, the specified relationships will be followed and each record returned by the queryset will have the corresponding related objects already selected and populated. Using `#join` can result in performance improvements since it can help reduce the number of SQL queries, as illustrated by the following example: ```crystal query_set = Post.all @@ -155,13 +185,17 @@ p2 = query_set.join(:author).get(id: 1) puts p2.author # doesn't hit the database since the related "author" was already selected ``` -It should be noted that it is also possible to follow foreign keys of direct related models too by using the double underscores notation (`__`). For example the following query will select the joined "author" and its associated "profile": +It should be noted that it is also possible to follow foreign keys of direct related models too by using the double underscores notation (`__`). For example, the following query will select the joined "author" and its associated "profile": ```crystal query_set = Post.all query_set.join(:author__profile) ``` +:::info +The `#join` method also supports targeting the reverse relation of a [`one_to_one`](./fields.md#one_to_one) field (such reverse relation can be defined through the use of the [`related`](./fields.md#related-2) field option). That way, you can traverse a [`one_to_one`](./fields.md#one_to_one) field back to the model record on which the field is specified. +::: + ### `none` Returns a query set that will always return an empty array of records, without querying the database. @@ -186,11 +220,48 @@ query_set.order("-published_at", "title") In the above example, records would be ordered by descending publication date (because of the `-` prefix), and then by title (ascending). +### `prefetch` + +Returns a query set that will automatically prefetch in a single batch the records for the specified relations (see [Queries](../queries.md#pre-fetching-relations) for an introduction about this capability). + +When using `#prefetch`, the records corresponding to the specified relationships will be prefetched in single batches and each record returned by the query set will have the corresponding related objects already selected and populated. Using `#prefetch` can result in performance improvements since it can help reduce the number of SQL queries, as illustrated by the following example: + +```crystal +posts_1 = Post.all.to_a +# hits the database to retrieve the related "tags" (many-to-many relation) +puts posts_1[0].tags.to_a + +posts_2 = Post.all.prefetch(:tags).to_a +# doesn't hit the database since the related "tags" relation was already prefetched +puts posts_2[0].tags +``` + +It should be noted that it is also possible to follow relations and reverse relations too by using the double underscores notation(`__`). For example, the following query will prefetch the "author" relation and then the "favorite tags" relation of the author records: + +```crystal +query_set = Post.all +query_set.prefetch(:author__favorite_tags) +``` + +Finally, it is worth mentioning that multiple relations can be specified to `#prefetch`. For example: + +```crystal +Author.all.prefetch(:books__genres, :publisher) +``` + +:::tip +The `#prefetch` method can also be called directly on model classes: + +```crystal +Author.prefetch(:books__genres, :publisher) +``` +::: + ### `raw` Returns a raw query set for the passed SQL query and optional parameters. -This method returns a [`Marten::DB::Query::RawSet`](pathname:///api/0.2/Marten/DB/Query/RawSet.html) object, which allows to iterate over the model records matched by the passed SQL query. For example: +This method returns a [`Marten::DB::Query::RawSet`](pathname:///api/0.5/Marten/DB/Query/RawSet.html) object, which allows to iterate over the model records matched by the passed SQL query. For example: ```crystal Article.all.raw("SELECT * FROM articles") @@ -252,6 +323,89 @@ The value passed to `#using` must be a valid database alias that was used to con Query sets also provide a set of methods that will usually result in specific SQL queries to be executed in order to return values that don't correspond to new query sets. +### `average` + +Allows calculating the average of a numeric field within the records of a specific model. The `#average` method can be used as a class method from any model class, or it can be used as an instance method from any query set object. When used on a query set, it calculates the average of the specified field for the records in that query set. + +For example: + +```crystal +average_price = Product.average(:price) # Calculate the average price of all products + +# Calculate the average rating for a specific category of products +electronic_products = Product.filter(category: "Electronics") +average_rating = electronic_products.average(:rating) +``` + +### `build` + +Initializes a new model instance. + +This method allows initializing a new model instance using the arguments defined in the passed double splat argument. + +```crystal +new_post = Post.all.build(title: "My blog post") +``` + +This method can also be called with a block that is executed for the new object: + +```crystal +new_post = Post.all.build(title: "My blog post") do |p| + p.complex_attribute = compute_complex_attribute +end +``` + +### `bulk_create` + +Bulk inserts the passed model instances into the database. + +This method allows to insert multiple model instances into the database in a single query. This can be useful when dealing with large amounts of data that need to be inserted into the database. For example: + +```crystal +query_set = Post.all +query_set.bulk_create( + [ + Post.new(title: "First post"), + Post.new(title: "Second post"), + Post.new(title: "Third post"), + ] +) +``` + +An optional `batch_size` argument can be passed to this method in order to specify the number of records that should be inserted in a single query. By default, all records are inserted in a single query (except for SQLite databases where the limit of variables in a single query is 999). For example: + +```crystal +query_set = Post.all +query_set.bulk_create( + [ + Post.new(title: "First post"), + Post.new(title: "Second post"), + Post.new(title: "Third post"), + ], + batch_size: 2 +) +``` + +:::tip +The `#bulk_create` method can also be called directly on model classes: + +```crystal +Post.bulk_create( + [ + Post.new(title: "First post"), + Post.new(title: "Second post"), + Post.new(title: "Third post"), + ] +) +``` +::: + +It is worth mentioning that this method has a few caveats: + +* The specified records are assumed to be valid and no [callbacks](../callbacks.md) will be called on them. +* Bulk-creating records making use of multi-table inheritance is not supported. +* If the model's primary key field is auto-incremented at the database level, the newly inserted primary keys will only be assigned to records on certain databases that support retrieving bulk-inserted rows (namely MariaDB, PostgreSQL, and SQLite). + ### `count` Returns the number of records that are targeted by the current query set. @@ -260,6 +414,7 @@ For example: ```crystal Article.all.count # returns the number of article records +Article.all.count(:subtitle) # returns the number of articles where the subtitle is not null Article.filter(title__startswith: "Top").count # returns the number of articles whose title start with "Top" ``` @@ -296,7 +451,7 @@ query_set = Post.all query_set.create!(title: "My blog post") ``` -This method can also be called with block that is executed for the new object. This block can be used to directly initialize the object before it is persisted to the database: +This method can also be called with a block that is executed for the new object. This block can be used to directly initialize the object before it is persisted to the database: ```crystal query_set = Post.all @@ -338,6 +493,14 @@ Article.filter(title__startswith: "Top").exists? Note that this method will trigger a very simple `SELECT EXISTS` SQL query if the query set was not already evaluated: when this happens, no model records will be instantiated since the records existence will be determined at the database level. If the query set was already evaluated, the underlying array of records will be used to determine if records exist or not. +It should be noted that `#exists?` can also take additional filters or `q()` expressions as arguments. This allows to apply additional filters to the considered query set in order to perform the check. For example: + +```crystal +query_set = Tag.filter(name__startswith: "c") +query_set.exists?(is_active: true) +query_set.exists? { q(is_active: true) } +``` + ### `first` Returns the first record that is matched by the query set, or `nil` if no records are found. @@ -400,6 +563,62 @@ post_2 = query_set.get! { q(id: 456, is_published: false) } If the specified set of filters doesn't match any records, a `Marten::DB::Errors::RecordNotFound` exception will be raised. Moreover, in order to ensure data consistency this method will raise a `Marten::DB::Errors::MultipleRecordsFound` exception if multiple records match the specified set of filters. +### `get_or_create` + +Returns the model record matching the given set of filters or create a new one if no one is found. + +Model fields that uniquely identify a record should be used here. For example: + +```crystal +tag = Tag.all.get_or_create(label: "crystal") +``` + +When no record is found, the new model instance is initialized by using the attributes defined in the double splat arguments. Regardless of whether it is valid or not (and thus persisted to the database or not), the initialized model instance is returned by this method. + +This method can also be called with a block that is executed for new objects. This block can be used to directly initialize new records before they are persisted to the database: + +```crystal +tag = Tag.all.get_or_create(label: "crystal") do |new_tag| + new_tag.active = false +end +``` + +In order to ensure data consistency, this method will raise a `Marten::DB::Errors::MultipleRecordsFound` exception if multiple records match the specified set of filters. + +### `get_or_create!` + +Returns the model record matching the given set of filters or create a new one if no one is found. + +Model fields that uniquely identify a record should be used here. For example: + +```crystall +tag = Tag.all.get_or_create!(label: "crystal") +``` + +When no record is found, the new model instance is initialized by using the attributes defined in the double splat arguments. If the new model instance is valid, it is persisted to the database ; otherwise a `Marten::DB::Errors::InvalidRecord` exception is raised. + +This method can also be called with a block that is executed for new objects. This block can be used to directly initialize new records before they are persisted to the database: + +```crystal +tag = Tag.all.get_or_create!(label: "crystal") do |new_tag| + new_tag.active = false +end +``` + +In order to ensure data consistency, this method will raise a `Marten::DB::Errors::MultipleRecordsFound` exception if multiple records match the specified set of filters. + +### `includes?` + +Returns `true` if a specific model record is included in the query set. + +This method can be used to verify the membership of a specific model record in a given query set. If the query set is not evaluated yet, a dedicated SQL query will be executed in order to perform this check (without loading the entire list of records that are targeted by the query set). This is especially interesting for large query sets where we don't want all the records to be loaded in memory in order to perform such check. + +```crystal +tag = Tag.get!(name: "crystal") +query_set = Tag.filter(name__startswith: "c") +query_set.includes?(tag) # => true +``` + ### `last` Returns the last record that is matched by the query set, or `nil` if no records are found. @@ -418,6 +637,24 @@ Article.last! Article.filter(title__startswith: "Top").last! ``` +### `maximum` + +Retrieves the maximum value in a specific field across all records within a query set. + +```crystal +Product.all.maximum(:price) # Retrieves the highest price across all products +# => 125.25 +``` + +### `minimum` + +Retrieves the minimum value in a specific field across all records within a query set. + +```crystal +Product.all.minimum(:price) # Retrieves the lowest price across all products +# => 15.99 +``` + ### `paginator` Returns a paginator that can be used to paginate the current query set. @@ -458,6 +695,18 @@ Post.filter(pk: 1).pick!("title", "published") # => ["First article", true] ``` +### `pks` + +Returns the primary key values of the considered model records targeted by the current query set. + +This method returns an array containing the primary key values of the model records that are targeted by the current query set. + +For example: + +```crystal +Post.all.pks # => [1, 2, 3] +``` + ### `pluck` Returns specific column values without loading entire record objects. @@ -475,6 +724,36 @@ Post.all.pluck("title", "published") Alias for [`#count`](#count): returns the number of records that are targetted by the query set. +### `sum` + +Calculates the total sum of values in a specific field across all records within a query set. + +Example: + +```crystal +Order.all.sum(:amount) # Calculates the total amount across all orders +# => 7 +``` + +### `to_s` + +Returns a string representation of the considered query set. + +### `to_sql` + +Returns the SQL representation of the considered query set. + +For example: + +```crystal +Tag.filter(name__startswith: "r").to_sql +# => "SELECT app_tag.id, app_tag.name, app_tag.is_active FROM \"app_tag\" WHERE app_tag.name LIKE $1" +``` + +:::note +The outputted SQL will vary depending on the database backend in use. +::: + ### `update` Updates all the records matched by the current query set with the passed values. @@ -575,6 +854,13 @@ Allows filtering records based on field values that are contained in a specific Tag.all.filter(slug__in=["foo", "bar", "xyz"]) ``` +Note that this predicate can also be used for filtering relation fields (such as [`many_to_one`](./fields.md#many_to_one) or [`one_to_one`](./fields.md#one_to_one) fields) using arrays of model records. For example: + +```crystal +authors = Author.filter(first_name: "John") +articles = Article.filter(author__in: authors) +``` + ### `isnull` Allows filtering records based on field values that should be null or not null. diff --git a/docs/versioned_docs/version-0.5/models-and-databases/reference/table-options.md b/docs/versioned_docs/version-0.5/models-and-databases/reference/table-options.md new file mode 100644 index 000000000..efe1d6d94 --- /dev/null +++ b/docs/versioned_docs/version-0.5/models-and-databases/reference/table-options.md @@ -0,0 +1,57 @@ +--- +title: Table options +description: Table options reference. +--- + +This page provides a reference for all the table options that can be leveraged when defining models. + +## Table name + +Table names for models are automatically generated from the model name and the label of the associated application. That being said, it is possible to specifically override the name of a model table by leveraging the [`#db_table`](pathname:///api/0.5/Marten/DB/Model/Table/ClassMethods.html#db_table(db_table%3AString|Symbol)-instance-method) class method, which requires a table name string or symbol. + +For example: + +```crystal +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :content, :text + +// highlight-next-line + db_table :articles +end +``` + +## Table indexes + +Multifields indexes can be configured in a model by leveraging the [`#db_index`](pathname:///api/0.5/Marten/DB/Model/Table/ClassMethods.html#db_index(name%3AString|Symbol%2Cfield_names%3AArray(String)|Array(Symbol))%3ANil-instance-method) class method. This method requires an index name argument as well as an array of targeted field names. + +For example: + +```crystal +class Person < Marten::Model + field :id, :int, primary_key: true, auto: true + field :first_name, :string, max_size: 50 + field :last_name, :string, max_size: 50 + +// highlight-next-line + db_index :person_full_name_index, field_names: [:first_name, :last_name] +end +``` + +## Table unique constraints + +Multifields unique constraints can be configured in a model by leveraging the [`#db_unique_constraint`](pathname:///api/0.5/Marten/DB/Model/Table/ClassMethods.html#db_unique_constraint(name%3AString|Symbol%2Cfield_names%3AArray(String)|Array(Symbol))%3ANil-instance-method) class method. This method requires an index name argument as well as an array of targeted field names. + +For example: + +```crystal +class Booking < Marten::Model + field :id, :int, primary_key: true, auto: true + field :room, :string, max_size: 50 + field :date, :date, max_size: 50 + +// highlight-next-line + db_unique_constraint :booking_room_date_constraint, field_names: [:room, :date] +end +``` diff --git a/docs/versioned_docs/version-0.5/models-and-databases/relationships.md b/docs/versioned_docs/version-0.5/models-and-databases/relationships.md new file mode 100644 index 000000000..6cbbbbf12 --- /dev/null +++ b/docs/versioned_docs/version-0.5/models-and-databases/relationships.md @@ -0,0 +1,429 @@ +--- +title: Relationships +description: Learn how to define relationships in models. +--- + +Marten offers a powerful and intuitive solution for defining the three most common types of database relationships (many-to-one, one-to-one, and many-to-many) through the use of [model fields](./introduction.md#model-fields). By leveraging these special fields, developers can enhance their application's data modeling and streamline data access. + +## Many-to-one relationships + +Many-to-one relationships can be defined through the use of [`many_to_one`](./reference/fields.md#many_to_one) fields. This special field type requires the utilization of the [`to`](./reference/fields.md#to-1) argument, allowing to explicitly define the target model class associated with the current model. + +For example, an `Article` model could have a many-to-one field towards an `Author` model. In such case, an `Article` record would only have one associated `Author` record, but every `Author` record could be associated with many `Article` records: + +```crystal +class Author < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :full_name, :string, max_size: 128 +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 128 + // highlight-next-line + field :author, :many_to_one, to: Author +end +``` + +### Interacting with related records + +Like for any other [model fields](./introduction.md#model-fields), Marten automatically generates getters and setters allowing to interact with the field's value. + +With the above snippet, it would be possible to access the `Author` record associated with a specific `Article` record by leveraging the `#author` and `#author=` methods. For example: + +```crystal +# Create two authors +author_1 = Author.create!(full_name: "Foo Bar") +author_2 = Author.create!(full_name: "John Doe") + +# Create an article +article = Article.create!(title: "First article", author: author_1) +article.author!.id # => 1 +article.author # => # + +# Change the article author +article.author = author_2 +article.save! +article.author!.id # => 2 +article.author # => # +``` + +:::tip +Note that you can also access the related record's ID directly without actually loading it by leveraging the `#_id` method (which corresponds to the actual name of the column used to persist the reference to the related record's primary key in the model table). + +For instance, using the model definitions provided earlier, you could perform the following operation: + +```crystal +author = Author.create!(full_name: "Foo Bar") +article = Article.create!(title: "First article", author: author) +article.author_id # => 1 +``` +::: + +### Backward relations + +By default, [`many_to_one`](./reference/fields.md#many_to_one) fields do not establish a backward relation. This means that you cannot directly retrieve records that target a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve all the `Article` records associated with a specific `Author` record. + +To enable this capability, you need to make use of the [`related`](./reference/fields.md#related-1) argument when defining your [`many_to_one`](./reference/fields.md#many_to_one) field. For instance, we could modify the previous model definitions as follows in order to define an `articles` backward relation and to let `Author` records expose their related `Article` records: + +```crystal +class Author < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :full_name, :string, max_size: 128 +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 128 + // highlight-next-line + field :author, :many_to_one, to: Author, related: :articles +end +``` + +When the [`related`](./reference/fields.md#related-1) argument is used, a method will be automatically created on the targetted model by using the chosen argument's value. For example, this means that all the `Article` records associated with a specific `Author` record will be accessible through the use of the `Author#articles` method: + +```crystal +# Create two authors +author_1 = Author.create!(full_name: "Foo Bar") +author_2 = Author.create!(full_name: "John Doe") + +# Create articles +article_1 = Article.create!(title: "First article", author: author_1) +article_2 = Article.create!(title: "Second article", author: author_2) +article_3 = Article.create!(title: "Third article", author: author_1) + +# List the first author's articles +author_1.articles.to_a # [#, + # #] + +# Create an article associated with the first author +article_4 = author_1.articles.create!(title: "Fourth article") +article_4.author # => # +``` + +:::tip +The method generated for the backward relation returns a [query set](./queries.md) that you can use to further filter the list of records. For example: + +```crystal +author.articles.filter(title__startswith: "Top") +``` +::: + +### Deletion strategy + +When defining [`many_to_one`](./reference/fields.md#many_to_one) fields, it is highly advisable to specify a deletion strategy for the associated relation. This configuration determines the behavior of records with many-to-one fields when one of the records referred to by such fields gets deleted. + +Such behavior can be configured by leveraging the [`on_delete`](./reference/fields.md#on_delete) argument when defining [`many_to_one`](./reference/fields.md#many_to_one) fields. This argument allows specifying the deletion strategy to adopt when a related record (one that is targeted by the [`many_to_one`](./reference/fields.md#many_to_one) field) is deleted. This argument accepts the following values (expressed as symbols): + +* `:do_nothing`: This is the default strategy. With this strategy, Marten won't do anything to ensure that records referencing the record being deleted are deleted or updated. If the database enforces referential integrity (which will be the case for foreign key fields), this means that deleting a record could result in database errors. +* `:cascade`: This strategy can be used to perform cascade deletions. When deleting a record, Marten will try to first destroy the other records that reference the object being deleted. +* `:protect`: This strategy allows explicitly preventing the deletion of records if they are referenced by other records. This means that attempting to delete a "protected" record will result in a `Marten::DB::Errors::ProtectedRecord` error. +* `:set_null`: This strategy will set the reference column to `null` when the related record is deleted. + +For example, we could modify our previous model definition so that `Article` records are cascade-deleted if the associated `Author` records are destroyed: + +```crystal +class Author < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :full_name, :string, max_size: 128 +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 128 + // highlight-next-line + field :author, :many_to_one, to: Author, related: :articles, on_delete: :cascade +end +``` + +With this change, if we try to delete an `Author` record, we should notice that the associated `Article` records are deleted as well: + +```crystal +# Create two authors +author_1 = Author.create!(full_name: "Foo Bar") +author_2 = Author.create!(full_name: "John Doe") + +# Create articles +article_1 = Article.create!(title: "First article", author: author_1) +article_2 = Article.create!(title: "Second article", author: author_2) +article_3 = Article.create!(title: "Third article", author: author_1) + +# Delete the first author +author_1.delete + +article_1.reload # => raises Marten::DB::Errors::RecordNotFound +``` + +## One-to-one relationships + +One-to-one relationships can be defined through the use of [`one_to_one`](./reference/fields.md#one_to_one) fields. This special field type requires the utilization of the [`to`](./reference/fields.md#to-2) argument, allowing to explicitly define the target model class associated with the current model. + +For example, a `User` model could have a one-to-one field towards a `Profile` model. In such case, the `User` model could only have one associated `Profile` record, and the reverse would be true as well (a `Profile` record could only have one associated `User` record): + +```crystal +class Profile < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :full_name, :string, max_size: 128 +end + +class User < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :email, :email + // highlight-next-line + field :profile, :one_to_one, to: Profile +end +``` + +:::info +A one-to-one field is really similar to a many-to-one field, but with an additional unicity constraint. +::: + +### Interacting with related records + +Like for any other [model fields](./introduction.md#model-fields), Marten automatically generates getters and setters allowing to interact with the field's value. + +With the above snippet, it would be possible to access the `Profile` record associated with a specific `User` record by leveraging the `#profile` and `#profile=` methods. For example: + +```crystal +# Create two users +user_1 = User.create!(email: "test1@example.com", profile: Profile.create!(full_name: "Foo Bar")) +user_2 = User.create!(email: "test2@example.com", profile: Profile.create!(full_name: "John Doe")) + +# Access a user's profile +user_1.profile!.id # => 1 +user_1.profile # => # + +# Change a user's profile +user_1.profile = Profile.create!(full_name: "New Profile") +user_1.save! +user_1.profile!.id # => 3 +user_1.profile # => # +``` + +:::tip +Like for [many-to-one relationships](#many-to-one-relationships), you can also access the related record's ID directly without actually loading it by leveraging the `#_id` method (which corresponds to the actual name of the column used to persist the reference to the related record's primary key in the model table). + +For instance, using the model definitions provided earlier, you could perform the following operation: + +```crystal +user = User.create!(email: "test1@example.com", profile: Profile.create!(full_name: "Foo Bar")) +user.profile_id # => 1 +``` +::: + +### Backward relations + +By default, [`one_to_one`](./reference/fields.md#one_to_one) fields do not establish a backward relation. This means that you cannot directly retrieve the record that targets a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve the `User` record associated with a specific `Profile` record. + +To enable this capability, you need to make use of the [`related`](./reference/fields.md#related-2) argument when defining your [`one_to_one`](./reference/fields.md#one_to_one) field. For instance, we could modify the previous model definitions as follows in order to define a `user` backward relation and to let `Profile` records expose their related `User` record: + +```crystal +class Profile < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :full_name, :string, max_size: 128 +end + +class User < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :email, :email + // highlight-next-line + field :profile, :one_to_one, to: Profile, related: :user +end +``` + +When the [`related`](./reference/fields.md#related-2) argument is used, a method will be automatically created on the targetted model by using the chosen argument's value. For example, this means that the `User` record associated with a specific `Profile` record will be accessible through the use of the `Profile#user` method: + +```crystal +# Create two profiles +profile_1 = Profile.create!(full_name: "Foo Bar") +profile_2 = Profile.create!(full_name: "John Doe") + +# Create two users +user_1 = User.create!(email: "test1@example.com", profile: profile_1) +user_2 = User.create!(email: "test2@example.com", profile: profile_2) + +# Get the first profile's user +profile_1.user # => # +``` + +:::tip +Note that in the previous example, `#user` could return `nil` if no `User` record is available for the considered profile. A nil-safe version of the related method is also automatically defined with the following name: `#!`. For example: + +```crystal +# Create two profiles +profile_1 = Profile.create!(full_name: "Foo Bar") +profile_2 = Profile.create!(full_name: "John Doe") + +# Create two users +user_1 = User.create!(email: "test1@example.com", profile: profile_1) +user_2 = User.create!(email: "test2@example.com", profile: profile_2) + +# Delete the first user +user_1.delete + +# Get the first profile's user +profile_1.user! # => raises Marten::DB::Errors::RecordNotFound +``` +::: + +### Deletion strategy + +Like for [many-to-one relationships](#deletion-strategy), the deletion strategy to use for [`one_to_one`](./reference/fields.md#one_to_one) fields can be configured by leveraging the [`on_delete`](./reference/fields.md#on_delete-1) argument. This argument allows specifying the deletion strategy to adopt when a related record (one that is targeted by the [`many_to_one`](./reference/fields.md#many_to_one) field) is deleted. This argument accepts the following values (expressed as symbols): + +* `:do_nothing`: This is the default strategy. With this strategy, Marten won't do anything to ensure that the record referencing the record being deleted is deleted or updated. If the database enforces referential integrity (which will be the case for foreign key fields), this means that deleting a record could result in database errors. +* `:cascade`: This strategy can be used to perform cascade deletions. When deleting a record, Marten will try to first destroy the other record that references the object being deleted. +* `:protect`: This strategy allows explicitly preventing the deletion of the record if is is referenced by another record. This means that attempting to delete a "protected" record will result in a `Marten::DB::Errors::ProtectedRecord` error. +* `:set_null`: This strategy will set the reference column to `null` when the related record is deleted. + +For example, we could modify our previous model definition so that a `User` record is cascade-deleted if the associated `Profile` records is destroyed: + +```crystal +class Profile < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :full_name, :string, max_size: 128 +end + +class User < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :email, :email + // highlight-next-line + field :profile, :one_to_one, to: Profile, related: :user, on_delete: :cascade +end +``` + +With this change, if we try to delete a `Profile` record, we should notice that the associated `User` records is deleted as well: + +```crystal +# Create two profiles +profile_1 = Profile.create!(full_name: "Foo Bar") +profile_2 = Profile.create!(full_name: "John Doe") + +# Create two users +user_1 = User.create!(email: "test1@example.com", profile: profile_1) +user_2 = User.create!(email: "test2@example.com", profile: profile_2) + +# Delete the first profile +profile_1.delete + +user_1.reload # => raises Marten::DB::Errors::RecordNotFound +``` + +## Many-to-many relationships + +Many-to-many relationships can be defined through the use of [`many_to_many`](./reference/fields.md#many_to_many) fields. This special field type requires the utilization of the [`to`](./reference/fields.md#to) argument, allowing to explicitly define the target model class associated with the current model. + +For example, an `Article` model could have a many-to-many field towards a `Tag` model. In such case, an `Article` record could have many associated `Tag` records, and every `Tag` record could be associated with many `Article` records as well: + +```crystal +class Tag < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :label, :string, max_size: 128 +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 128 + // highlight-next-line + field :tags, :many_to_many, to: Tag +end +``` + +### Interacting with related records + +[`many_to_many`](./reference/fields.md#many_to_many) fields exhibit unique characteristics compared to other relationship fields. When using [`many_to_many`](./reference/fields.md#many_to_many) fields in Marten, the framework generates a `#` getter method that returns a specialized [query set](./queries.md) that not only enables filtering of targeted records but also facilitates the dynamic addition and removal of records to/from the set. + +With the above snippet, it would be possible to access the `Tags` records associated with a specific `Article` record by leveraging the `#tags` method. For example: + +```crystal +# Create three tags +tag_1 = Tag.create!(label: "Tag 1") +tag_2 = Tag.create!(label: "Tag 2") +tag_3 = Tag.create!(label: "Tag 3") + +# Create one article +article = Article.create!(title: "My article") + +# Add one tag to the article +article.tags.add(tag_1) +article.tags.to_a # => [#] + +# Add two tags to the article +article.tags.add(tag_2, tag_3) +article.tags.to_a # => [#, + # #, + # #] + +# Filter the article's tags +article.tags.filter(label: "Tag 1").to_a # => [#] + +# Remove a tag from the article's tags +article.tags.remove(tag_2) +article.tags.to_a # => [#, + # #] + +# Clear the article's tags +article.tags.clear +``` + +Take note of the utilization of the [`#add`](pathname:///api/0.5/Marten/DB/Query/ManyToManySet.html#add(*objs%3AM)-instance-method) and [`#remove`](pathname:///api/0.5/Marten/DB/Query/ManyToManySet.html#remove(*objs%3AM)%3ANil-instance-method) methods, facilitating the addition or removal of objects from the record's many-to-many collection of associated items. These methods are callable with single or multiple records as parameters, as well as with arrays of records for streamlined addition or removal. + +### Backward relations + +By default, [`many_to_many`](./reference/fields.md#many_to_many) fields do not establish a backward relation. This means that you cannot directly retrieve records that target a specific related record starting from the related record itself. For instance, by default, it is not possible to retrieve all the `Article` records associated with a specific `Tag` record. + +To enable this capability, you need to make use of the [`related`](./reference/fields.md#related-1) argument when defining your [`many_to_many`](./reference/fields.md#many_to_many) field. For instance, we could modify the previous model definitions as follows in order to define an `articles` backward relation and to let `Tag` records expose their related `Article` records: + +```crystal +class Tag < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :label, :string, max_size: 128 +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 128 + // highlight-next-line + field :tags, :many_to_many, to: Tag, related: :articles +end +``` + +When the [`related`](./reference/fields.md#related) argument is used, a method will be automatically created on the targetted model by using the chosen argument's value. For example, this means that all the `Article` records associated with a specific `Tag` record will be accessible through the use of the `Tag#articles` method: + +```crystal +# Create three tags +tag_1 = Tag.create!(label: "Tag 1") +tag_2 = Tag.create!(label: "Tag 2") +tag_3 = Tag.create!(label: "Tag 3") + +# Create two articles +article_1 = Article.create!(title: "First article") +article_2 = Article.create!(title: "Second article") + +# Add tags to the articles +article_1.tags.add(tag_1, tag_2) +article_2.tags.add(tag_2, tag_3) + +# Retrieve the second tag's articles +tag_2.articles.to_a # => [#, + # #] +tag_2.articles.filter(title: "First article").to_a # => [#] +``` + +## Advanced topics + +### Recursive relationships + +All the relationship fields mentioned previously support defining recursive relations, ie. relations that target the same model as the model defining the relation field. To do so, you can define a [`many_to_one`](./reference/fields.md#many_to_one), [`one_to_one`](./reference/fields.md#one_to_one), or [`many_to_many`](./reference/fields.md#many_to_many) field whose `to` argument is set to the `self` keyword. + +For example: + +```crystal +class TreeNode < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :label, :string, max_size: 128 + // highlight-next-line + field :parent, :many_to_one, to: self +end +``` + +In the above snippet, the `TreeNode` model will have a relation to itself through the `parent` field. diff --git a/docs/versioned_docs/version-0.2/models-and-databases/transactions.md b/docs/versioned_docs/version-0.5/models-and-databases/transactions.md similarity index 89% rename from docs/versioned_docs/version-0.2/models-and-databases/transactions.md rename to docs/versioned_docs/version-0.5/models-and-databases/transactions.md index cf1c43f19..ba0065a75 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/transactions.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/transactions.md @@ -8,7 +8,7 @@ Transactions are blocks whose underlying SQL statements are committed to the dat ## The basics -Transactions are essential in order to enforce database integrity. Whenever you are in a situation where you have more than one SQL operations that must be executed together or not at all, then you should consider wrapping all these operations in a dedicated transaction. Transaction blocks can be created by leveraging the `#transaction` method, which can be called either on [model records](pathname:///api/0.2/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [model classes](pathname:///api/0.2/Marten/DB/Model/Connection/ClassMethods.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26)-instance-method). +Transactions are essential in order to enforce database integrity. Whenever you are in a situation where you have more than one SQL operations that must be executed together or not at all, then you should consider wrapping all these operations in a dedicated transaction. Transaction blocks can be created by leveraging the `#transaction` method, which can be called either on [model records](pathname:///api/0.5/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [model classes](pathname:///api/0.5/Marten/DB/Model/Connection/ClassMethods.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26)-instance-method). For example: @@ -21,7 +21,7 @@ end With the above snippet, both records will be saved _only_ if each save operation completes successfully (that is if no exception is raised). If an exception occurs as part of one of the save operations (eg. if one of the records is invalid), then no records will be saved. -It should be noted that there is no difference between calling `#transaction` on [a model record](pathname:///api/0.2/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [a model class](pathname:///api/0.2/Marten/DB/Model/Connection/ClassMethods.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26)-instance-method). It's also worth mentioning that the models manipulated within a transaction block that result in SQL statements can be of different classes. For example, the following two transactions would be equivalent: +It should be noted that there is no difference between calling `#transaction` on [a model record](pathname:///api/0.5/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [a model class](pathname:///api/0.5/Marten/DB/Model/Connection/ClassMethods.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26)-instance-method). It's also worth mentioning that the models manipulated within a transaction block that result in SQL statements can be of different classes. For example, the following two transactions would be equivalent: ```crystal MyModel.transaction do @@ -43,13 +43,13 @@ When transaction blocks are nested, this results in all the database statements Basic model operations such as [creating](./introduction.md#create), [updating](./introduction.md#update), or [deleting](./introduction.md#delete) records are automatically wrapped in a transaction. This helps in ensuring that any exception that is raised in the context of validations or as part of `after_*` [callbacks](./callbacks.md) (ie. `after_create`, `after_update`, `after_save`, and `after_delete`) will also roll back the current transaction. -The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#aftercommit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). +The consequence of this is that the changes you make to the database in these callbacks will not be "visible" until the transaction is complete. For example, this means that if you are triggering something (like an asynchronous job) that needs to leverage the changes introduced by a model operation, then you should probably not use the regular `after_*` callbacks. Instead, you should leverage [`after_commit`](./callbacks.md#after_commit) callbacks (which are the only callbacks that are triggered _after_ a model operation has been committed to the database). ## Exception handling and rollbacks As mentioned before, any exception that is raised from within a transaction block will result in the considered transaction being rolled back. Moreover, it should be noted that raised exceptions will also be propagated outside of the transaction block, which means that your codebase should catch these accordingly if applicable. -If you need to roll back a transaction _manually_ from within a transaction itself while ensuring that no exception is propagated outside of the block, then you can make use of the [`Marten::DB::Errors::Rollback`](pathname:///api/0.2/Marten/DB/Errors/Rollback.html) exception: when this specific exception is raised from inside a transaction block, the transaction will be rolled back and the transaction block will return `false`. +If you need to roll back a transaction _manually_ from within a transaction itself while ensuring that no exception is propagated outside of the block, then you can make use of the [`Marten::DB::Errors::Rollback`](pathname:///api/0.5/Marten/DB/Errors/Rollback.html) exception: when this specific exception is raised from inside a transaction block, the transaction will be rolled back and the transaction block will return `false`. For example: diff --git a/docs/versioned_docs/version-0.2/models-and-databases/validations.md b/docs/versioned_docs/version-0.5/models-and-databases/validations.md similarity index 99% rename from docs/versioned_docs/version-0.2/models-and-databases/validations.md rename to docs/versioned_docs/version-0.5/models-and-databases/validations.md index c47465823..92fa4294a 100644 --- a/docs/versioned_docs/version-0.2/models-and-databases/validations.md +++ b/docs/versioned_docs/version-0.5/models-and-databases/validations.md @@ -98,7 +98,7 @@ In the above snippet, a custom validation method ensures that the `name` of a `U Methods like `#valid?` or `#invalid?` only let you know whether a model instance is valid or invalid. But you'll likely want to know exactly what are the actual errors or how to add new ones. -As such, every model instance has an associated error set, which is an instance of [`Marten::Core::Validation::ErrorSet`](pathname:///api/0.2/Marten/Core/Validation/ErrorSet.html). +As such, every model instance has an associated error set, which is an instance of [`Marten::Core::Validation::ErrorSet`](pathname:///api/0.5/Marten/Core/Validation/ErrorSet.html). ### Inspecting errors diff --git a/docs/versioned_docs/version-0.2/prologue.md b/docs/versioned_docs/version-0.5/prologue.mdx similarity index 71% rename from docs/versioned_docs/version-0.2/prologue.md rename to docs/versioned_docs/version-0.5/prologue.mdx index b59535802..fb9a5617d 100644 --- a/docs/versioned_docs/version-0.2/prologue.md +++ b/docs/versioned_docs/version-0.5/prologue.mdx @@ -1,8 +1,33 @@ --- +hide_title: true +pagination_prev: null +pagination_next: null slug: / +title: Prologue --- -# Prologue +import logo from "./static/img/prologue/logo.png"; + +
+ logo +
+
+

Welcome to the Marten documentation!

+
**Marten** is a Crystal Web framework that enables pragmatic development and rapid prototyping. It provides a consistent and extensible set of tools that developers can leverage to build web applications without reinventing the wheel. @@ -21,4 +46,4 @@ The Marten documentation contains multiple pages and references that don't neces * **Reference pages** provide a curated technical reference of the framework APIs * **How-to guides** document how to solve common problems when working with the framework. Those can cover things like deployments, app development, etc -Additionally, an automatically-generated [API reference](pathname:///api/0.2/index.html) is also available to dig into Marten's internals. +Additionally, an automatically-generated [API reference](pathname:///api/0.5/index.html) is also available to dig into Marten's internals. diff --git a/docs/versioned_docs/version-0.2/schemas.mdx b/docs/versioned_docs/version-0.5/schemas.mdx similarity index 100% rename from docs/versioned_docs/version-0.2/schemas.mdx rename to docs/versioned_docs/version-0.5/schemas.mdx diff --git a/docs/versioned_docs/version-0.2/schemas/how-to/create-custom-schema-fields.md b/docs/versioned_docs/version-0.5/schemas/how-to/create-custom-schema-fields.md similarity index 97% rename from docs/versioned_docs/version-0.2/schemas/how-to/create-custom-schema-fields.md rename to docs/versioned_docs/version-0.5/schemas/how-to/create-custom-schema-fields.md index 1e7d5b342..500e6759a 100644 --- a/docs/versioned_docs/version-0.2/schemas/how-to/create-custom-schema-fields.md +++ b/docs/versioned_docs/version-0.5/schemas/how-to/create-custom-schema-fields.md @@ -21,7 +21,7 @@ When creating a custom schema field, there are usually two approaches that you c Regardless of the approach you take in order to define new schema field classes ([subclassing built-in fields](#subclassing-existing-schema-fields), or [creating new ones from scratch](#creating-new-schema-fields-from-scratch)), these classes must be registered to the Marten's global fields registry in order to make them available for use when defining schemas. -To do so, you will have to call the [`Marten::Schema::Field#register`](pathname:///api/0.2/Marten/Schema/Field.html#register(id%2Cfield_klass)-macro) method with the identifier of the field you wish to use, and the actual field class. For example: +To do so, you will have to call the [`Marten::Schema::Field#register`](pathname:///api/0.5/Marten/Schema/Field.html#register(id%2Cfield_klass)-macro) method with the identifier of the field you wish to use, and the actual field class. For example: ```crystal Marten::Schema::Field.register(:foo, FooField) @@ -43,7 +43,7 @@ The call to `#register` can be made from anywhere in your codebase, but obviousl This is probably the easiest way to create a custom field: if the field you want to create can be derived from one of the [built-in schema fields](../reference/fields.md) (usually those correspond to primitive types), then you can easily subclass the corresponding class and customize it so that it suits your needs. -For example, implementing a custom "email" field could be done by subclassing the existing [`Marten::Schema::Field::String`](pathname:///api/0.2/Marten/Schema/Field/String.html) class. Indeed, an "email" field is essentially a string with a pre-defined maximum size and some additional validation logic: +For example, implementing a custom "email" field could be done by subclassing the existing [`Marten::Schema::Field::String`](pathname:///api/0.5/Marten/Schema/Field/String.html) class. Indeed, an "email" field is essentially a string with a pre-defined maximum size and some additional validation logic: ```crystal class EmailField < Marten::Schema::Field::String @@ -75,7 +75,7 @@ Everything that is described in the following section about [creating schema fie ## Creating new schema fields from scratch -Creating new schema fields from scratch involves subclassing the [`Marten::Schema::Field::Base`](pathname:///api/0.2/Marten/Schema/Field/Base.html) abstract class. Because of this, the new field class is required to implement a set of mandatory methods. These mandatory methods, and some other ones that are optional (but interesting in terms of capabilities), are described in the following sections. +Creating new schema fields from scratch involves subclassing the [`Marten::Schema::Field::Base`](pathname:///api/0.5/Marten/Schema/Field/Base.html) abstract class. Because of this, the new field class is required to implement a set of mandatory methods. These mandatory methods, and some other ones that are optional (but interesting in terms of capabilities), are described in the following sections. ### Mandatory methods @@ -124,7 +124,7 @@ Again, if the value can't be processed properly by the field class, it may be ne #### `initialize` -The default `#initialize` method that is provided by the [`Marten::Schema::Field::Base`](pathname:///api/0.2/Marten/Schema/Field/Base.html) is fairly simply and looks like this: +The default `#initialize` method that is provided by the [`Marten::Schema::Field::Base`](pathname:///api/0.5/Marten/Schema/Field/Base.html) is fairly simply and looks like this: ```crystal def initialize( diff --git a/docs/versioned_docs/version-0.2/schemas/introduction.md b/docs/versioned_docs/version-0.5/schemas/introduction.md similarity index 63% rename from docs/versioned_docs/version-0.2/schemas/introduction.md rename to docs/versioned_docs/version-0.5/schemas/introduction.md index 70ae720ed..987461abf 100644 --- a/docs/versioned_docs/version-0.2/schemas/introduction.md +++ b/docs/versioned_docs/version-0.5/schemas/introduction.md @@ -10,7 +10,7 @@ Schemas are classes that define how input data should be serialized/deserialized ### The schema class -A schema class describes an _expected_ set of data. It describes the logical structure of this data, what are its expected characteristics, and what are the rules to use in order to identify whether it is valid or not. Schemas classes must inherit from the [`Marten::Schema`](pathname:///api/0.2/Marten/Schema.html) base class and they must define "fields" through the use of a `field` macro. These fields allow to define what data is expected by the schema, and how it is validated. +A schema class describes an _expected_ set of data. It describes the logical structure of this data, what are its expected characteristics, and what are the rules to use in order to identify whether it is valid or not. Schemas classes must inherit from the [`Marten::Schema`](pathname:///api/0.5/Marten/Schema.html) base class and they must define "fields" through the use of a `field` macro. These fields allow to define what data is expected by the schema, and how it is validated. For example, the following snippet defines a simple `ArticleSchema` schema: @@ -64,37 +64,8 @@ end Let's break it down a bit more: * when the incoming request is a `GET`, the handler will simply render the `article_create.html` template, and initialize the schema (instance of `ArticleSchema`) with any data currently present in the request object (which is returned by the `#request` method). This schema object is made available to the template context -* when the incoming request is a `POST`, it will initialize the schema and try to see if it is valid considering the incoming data (using the `#valid?` method). If it's valid, then a new `Article` record will be created using the schema's validated data (`#validated_data`), and the user will be redirect to a home page. Otherwise, the `article_create.html` template will be rendered again with the invalid schema in the associated context +* when the incoming request is a `POST`, it will initialize the schema and try to see if it is valid considering the incoming data (using the [`#valid?`](pathname:///api/0.5/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) method). If it's valid, then a new `Article` record will be created using the schema's validated data ([`#validated_data`](pathname:///api/0.5/Marten/Schema.html#validated_data%3AHash(String%2CBool|Float64|Int64|JSON%3A%3AAny|JSON%3A%3ASerializable|Marten%3A%3AHTTP%3A%3AUploadedFile|String|Time|Time%3A%3ASpan|UUID|Nil)-instance-method)), and the user will be redirect to a home page. Otherwise, the `article_create.html` template will be rendered again with the invalid schema in the associated context -It should be noted that templates can easily interact with schema objects in order to introspect them and render a corresponding HTML form. In the above example, the schema could be used as follows to render an equivalent form in the `article_create.html` template: - -```html -
- - -
-
- - {% for error in schema.title.errors %}

{{ error.message }}

{% endfor %} -
- -
-
- - {% for error in schema.content.errors %}

{{ error.message }}

{% endfor %} -
- -
-
- - {% for error in schema.published_at.errors %}

{{ error.message }}

{% endfor %} -
- -
- -
-
-``` :::tip Some [generic handlers](../handlers-and-http/generic-handlers.md) allow to conveniently process schemas in handlers. This is the case for the [`Marten::Handlers::Schema`](../handlers-and-http/reference/generic-handlers.md#processing-a-schema), the [`Marten::Handlers::RecordCreate`](../handlers-and-http/reference/generic-handlers.md#creating-a-record), and the [`Marten::Handlers::RecordUpdate`](../handlers-and-http/reference/generic-handlers.md#updating-a-record) generic handlers for example. @@ -106,7 +77,7 @@ Note that schemas can be used for other things than processing form data. For ex class API::ArticleCreateHandler < Marten::Handler def post schema = ArticleCreateHandler.new(request.data) - + if schema.valid? article = Article.new(schema.validated_data) article.save! @@ -125,6 +96,56 @@ end The `#data` method of an HTTP request object returns a hash-like object containing the request data: this object is automatically initialized from any form data or JSON data contained in the request body. ::: +### Rendering schemas as forms + +It should be noted that templates can easily interact with schema objects in order to introspect them and render a corresponding HTML form. + +In the previous example, the schema could be used as follows to render an equivalent form in the `article_create.html` template: + +```html +
+ + + {% if schema.errors.global %} +
+ {% for error in schema.errors.global %} +

{{ error.message }}

+ {% endfor %} +
+ {% endif %} + +
+
+ + {% for error in schema.title.errors %}

{{ error.message }}

{% endfor %} +
+ +
+
+ + {% for error in schema.content.errors %}

{{ error.message }}

{% endfor %} +
+ +
+
+ + {% for error in schema.published_at.errors %}

{{ error.message }}

{% endfor %} +
+ +
+ +
+
+``` + +The provided code also demonstrates how to efficiently handle errors within your templates. Here's a breakdown of the main guidelines to follow when dealing with schema errors in templates: + +* Begin by checking for any `schema.errors.global` errors. + These are form-wide issues, and displaying them prominently alerts the user to broader problems with their input. +* If `schema.field_name.errored?` returns true, it signals errors within that particular input field. +* Display each individual `schema.field_name.errors` message directly below the associated input field. + This provides the user with clear guidance on how to correct specific validation issues. + ## Schema fields Schema classes must define _fields_. Fields allow to specify the expected attributes of a schema and they indicate how to validate incoming data sets. They are defined through the use of the `field` macro. @@ -191,10 +212,47 @@ class SignUpSchema < Marten::Schema end ``` -Schema validations are always triggered by the use of the `#valid?` or `#invalid?` methods: these methods return `true` or `false` depending on whether the data is valid or invalid. +Schema validations are always triggered by the use of the [`#valid?`](pathname:///api/0.5/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) or [`#invalid?`](pathname:///api/0.5/Marten/Core/Validation.html#invalid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) methods: these methods return `true` or `false` depending on whether the data is valid or invalid. Please head over to the [Schema validations](./validations.md) guide in order to learn more about schema validations and how to customize it. +## Accessing validated data + +After performing [schema validations](#validations) (ie. after calling [`#valid?`](pathname:///api/0.5/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) or [`#invalid?`](pathname:///api/0.5/Marten/Core/Validation.html#invalid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) on a schema object), accessing the validated data is often necessary. For instance, you may need to persist the validated data as part of a model record. To achieve this, you can make use of the [`#validated_data`](pathname:///api/0.5/Marten/Schema.html#validated_data%3AHash(String%2CBool|Float64|Int64|JSON%3A%3AAny|JSON%3A%3ASerializable|Marten%3A%3AHTTP%3A%3AUploadedFile|String|Time|Time%3A%3ASpan|UUID|Nil)-instance-method) method, which is accessible in all schema instances. + +This method provides access to a hash that contains the deserialized and validated field values of the schema. For instance, let's consider the example of the `ArticleSchema` schema [mentioned earlier](#the-schema-class): + +```crystal +schema = ArticleSchema.new(Marten::Schema::DataHash{"title" => "Test article", "content" => "Test content"}) +schema.valid? # => true + +schema.validated_data["title"] # => "Test article" +schema.validated_data["content"] # => "Test content" +``` + +It is important to note that accessing values using [`#validated_data`](pathname:///api/0.5/Marten/Schema.html#validated_data%3AHash(String%2CBool|Float64|Int64|JSON%3A%3AAny|JSON%3A%3ASerializable|Marten%3A%3AHTTP%3A%3AUploadedFile|String|Time|Time%3A%3ASpan|UUID|Nil)-instance-method) as shown in the above example is not type-safe. The [`#validated_data`](pathname:///api/0.5/Marten/Schema.html#validated_data%3AHash(String%2CBool|Float64|Int64|JSON%3A%3AAny|JSON%3A%3ASerializable|Marten%3A%3AHTTP%3A%3AUploadedFile|String|Time|Time%3A%3ASpan|UUID|Nil)-instance-method) hash can return any supported schema field values, and as a result, you may need to utilize the [`#as`](https://crystal-lang.org/reference/syntax_and_semantics/as.html) pseudo-method to handle the fetched validated data appropriately, depending on how and where you intend to use it. + +To palliate this, Marten automatically defines type-safe methods that you can utilize to access your validated schema field values: + +* `#` returns a nillable version of the `` field value +* `#!` returns a non-nillable version of the `` field value +* `#?` returns a boolean indicating if the `` field has a value + +For example: + +```crystal +schema = ArticleSchema.new(Marten::Schema::DataHash{"title" => "Test article"}) +schema.valid? # => true + +schema.title # => "Test article" +schema.title! # => "Test article" +schema.title? # => true + +schema.content # => nil +schema.content! # => raises NilAssertionError +schema.content? # => false +``` + ## Callbacks It is possible to define callbacks in your schema in order to bind methods and logics to specific events in the life cycle of your schema objects. Presently, schemas support callbacks related to validation only: `before_validation` and `after_validation` @@ -220,4 +278,4 @@ class ArticleSchema < Marten::Schema end ``` -The use of methods like `#valid?` or `#invalid?` will trigger validation callbacks. See [Schema validations](./validations.md) for more details. +The use of methods like [`#valid?`](pathname:///api/0.5/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) or [`#invalid?`](pathname:///api/0.5/Marten/Core/Validation.html#invalid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) will trigger validation callbacks. See [Schema validations](./validations.md) for more details. diff --git a/docs/versioned_docs/version-0.2/schemas/reference/fields.md b/docs/versioned_docs/version-0.5/schemas/reference/fields.md similarity index 51% rename from docs/versioned_docs/version-0.2/schemas/reference/fields.md rename to docs/versioned_docs/version-0.5/schemas/reference/fields.md index d395f7088..94de7b123 100644 --- a/docs/versioned_docs/version-0.2/schemas/reference/fields.md +++ b/docs/versioned_docs/version-0.5/schemas/reference/fields.md @@ -27,6 +27,10 @@ A `date_time` field allows validating date time values. Fields using this type a A `date` field allows validating date values. Fields using this type are converted to `Time` objects in Crystal. +### `duration` + +A `duration` field allows validating duration values, which map to [`Time::Span`](https://crystal-lang.org/api/Time/Span.html) objects in Crystal. `duration` fields expect serialized values to be in the `DD.HH:MM:SS.nnnnnnnnn` format (with `n` corresponding to nanoseconds) or in the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) format (eg. `P3DT2H15M20S`, which corresponds to a `3.2:15:20` time span). + ### `email` An `email` field allows validating email address values. In addition to the [common field options](#common-field-options), such fields support the following arguments: @@ -43,6 +47,35 @@ The `min_size` argument allows defining the minimum size allowed for the email a The `strip` argument allows defining whether the string value should be stripped of leading and trailing whitespaces. The default is `true`. +### `enum` + +An `enum` field allows validating string values against the values of a specific [`Enum`](https://crystal-lang.org/api/Enum.html). When defining `enum` fields, it's necessary to specify a `values` argument that matches the actual enum: + +```crystal +enum Category + NEWS + BLOG +end + +class ArticleSchema < Marten::Schema + field :title, :string + field :category, :enum, values: Category +end + +schema = ArticleSchema.new( + Marten::HTTP::Params::Data{"title" => ["Test"], "category" => ["blog"]} +) + +schema.valid? # => true +schema.category # => Category::BLOG +``` + +In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `values` + +The `values` argument **is required** and allows to specify the actual enum class that should be used for the field. Only string values matching the values of the enum will be validated by the field. + ### `file` A `file` field allows validating uploaded files. In addition to the [common field options](#common-field-options), such fields support the following arguments: @@ -79,6 +112,48 @@ The `max_value` argument allows defining the maximum value allowed. The default The `min_value` argument allows defining the minimum value allowed. The default value for this argument is `nil`, which means that the minimum value is not validated by default. +### `json` + +A `json` field allows validating JSON values, which are automatically parsed to [`JSON::Any`](https://crystal-lang.org/api/JSON/Any.html) objects. Additionally, it is also possible to leverage the [`serializable`](#serializable) option in order to specify a class that makes use of [`JSON::Serializable`](https://crystal-lang.org/api/JSON/Serializable.html). When doing so, the parsing of the JSON values will result in the initialization of the corresponding serializable objects: + +```crystal +class MySerializable + include JSON::Serializable + + property a : Int32 | Nil + property b : String | Nil +end + +class MySchema < Marten::Schema + # Other fields... + field :metadata, :json, serializable: MySerializable +end + +schema = MySchema.new(Marten::Schema::DataHash{"metadata" => %{{"a": 42, "b": "foo"}}}) +schema.valid? # => true +schema.metadata! # => MySerializable object +``` + +#### `serializable` + +The `serializable` arguments allows to specify that a class making use of [`JSON::Serializable`](https://crystal-lang.org/api/JSON/Serializable.html) should be used in order to parse the JSON values for the schema field at hand. When specifying a `serializable` class, the values returned for the considered schema fields will be instances of that class instead of [`JSON::Any`](https://crystal-lang.org/api/JSON/Any.html) objects. + +### `slug` + +A `slug` field allows validating slug values (ie. strings that can only include characters, numbers, dashes, and underscores). In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `max_size` + +The `max_size` argument allows defining the maximum size allowed for the slug string. The default value for this argument is `50`. + +#### `min_size` + +The `min_size` argument allows defining the minimum size allowed for the slug string. The default value for this argument is `nil`, which means that the minimum size is not validated by default. + +#### `strip` + +The `strip` argument allows defining whether the string value should be stripped of leading and trailing whitespaces. The default is `true`. + ### `string` A `string` field allows validating string values. In addition to the [common field options](#common-field-options), such fields support the following arguments: @@ -98,3 +173,19 @@ The `strip` argument allows defining whether the string value should be stripped ### `uuid` A `uuid` field allows validating Universally Unique IDentifiers (UUID) values. Fields using this type are converted to `UUID` objects in Crystal. + +### `url` + +A `url` field allows validating URL address values. In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `max_size` + +The `max_size` argument allows defining the maximum size allowed for the URL string. The default value for this argument is `200`. + +#### `min_size` + +The `min_size` argument allows defining the minimum size allowed for the URL string. The default value for this argument is `nil`, which means that the minimum size is not validated by default. + +#### `strip` + +The `strip` argument allows defining whether the string value should be stripped of leading and trailing whitespaces. The default is `true`. diff --git a/docs/versioned_docs/version-0.2/schemas/validations.md b/docs/versioned_docs/version-0.5/schemas/validations.md similarity index 99% rename from docs/versioned_docs/version-0.2/schemas/validations.md rename to docs/versioned_docs/version-0.5/schemas/validations.md index 0e8ce3e84..cec719ffa 100644 --- a/docs/versioned_docs/version-0.2/schemas/validations.md +++ b/docs/versioned_docs/version-0.5/schemas/validations.md @@ -89,7 +89,7 @@ You can define multiple validation rules in your schema classes. When doing so, Methods like `#valid?` or `#invalid?` only let you know whether a schema instance is valid or invalid for a specific data set. But you'll likely want to know exactly what are the actual errors or how to add new ones. -As such, every schema instance has an associated error set, which is an instance of [`Marten::Core::Validation::ErrorSet`](pathname:///api/0.2/Marten/Core/Validation/ErrorSet.html). +As such, every schema instance has an associated error set, which is an instance of [`Marten::Core::Validation::ErrorSet`](pathname:///api/0.5/Marten/Core/Validation/ErrorSet.html). ### Inspecting errors diff --git a/docs/versioned_docs/version-0.2/security.mdx b/docs/versioned_docs/version-0.5/security.mdx similarity index 79% rename from docs/versioned_docs/version-0.2/security.mdx rename to docs/versioned_docs/version-0.5/security.mdx index aadd5ed5b..d6dd7ef64 100644 --- a/docs/versioned_docs/version-0.2/security.mdx +++ b/docs/versioned_docs/version-0.5/security.mdx @@ -18,4 +18,7 @@ Security is one of the most important topics to consider when developing web app
+
+ +
diff --git a/docs/versioned_docs/version-0.2/security/clickjacking.md b/docs/versioned_docs/version-0.5/security/clickjacking.md similarity index 97% rename from docs/versioned_docs/version-0.2/security/clickjacking.md rename to docs/versioned_docs/version-0.5/security/clickjacking.md index 01a6f0494..9f3e3c744 100644 --- a/docs/versioned_docs/version-0.2/security/clickjacking.md +++ b/docs/versioned_docs/version-0.5/security/clickjacking.md @@ -20,7 +20,7 @@ Marten's clickjacking protection involves using a dedicated middleware: the [X-F The [X-Frame-Options middleware](../handlers-and-http/reference/middlewares.md#x-frame-options-middleware) simply sets the X-Frame-Options header in order to prevent the considered Marten website from being inserted into a frame. The value that is used for the X-Frame-Options header depends on the value of the [`x_frame_options`](../development/reference/settings.md#x_frame_options) setting (whose default value is `DENY`). -It should be noted that you can decide to disable or enable the use of the [X-Frame-Options middleware](../handlers-and-http/reference/middlewares.md#x-frame-options-middleware) on a per-handler basis. To do so, you can simply make use of the [`#exempt_from_x_frame_options`](pathname:///api/0.2/Marten/Handlers/XFrameOptions/ClassMethods.html#exempt_from_x_frame_options(exempt%3ABool)%3ANil-instance-method) class method, which takes a single boolean as arguments: +It should be noted that you can decide to disable or enable the use of the [X-Frame-Options middleware](../handlers-and-http/reference/middlewares.md#x-frame-options-middleware) on a per-handler basis. To do so, you can simply make use of the [`#exempt_from_x_frame_options`](pathname:///api/0.5/Marten/Handlers/XFrameOptions/ClassMethods.html#exempt_from_x_frame_options(exempt%3ABool)%3ANil-instance-method) class method, which takes a single boolean as arguments: ```crystal class ProtectedHandler < Marten::Handler diff --git a/docs/versioned_docs/version-0.5/security/content-security-policy.md b/docs/versioned_docs/version-0.5/security/content-security-policy.md new file mode 100644 index 000000000..735c04c12 --- /dev/null +++ b/docs/versioned_docs/version-0.5/security/content-security-policy.md @@ -0,0 +1,94 @@ +--- +title: Content Security Policy +description: Learn how to configure the Content-Security-Policy (CSP) header. +--- + +Marten offers a convenient mechanism to define the Content-Security-Policy header, which serves as a safeguard against vulnerabilities such as cross-site scripting (XSS) and injection attacks. This mechanism enables the specification of a trusted resource allowlist, enhancing security measures. + +## Overview + +The [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) header is a collection of guidelines that the browser follows to allow specific sources for scripts, styles, embedded content, and more. It ensures that only these approved sources are allowed while blocking all other sources. + +Utilizing the Content-Security-Policy header in a web application is a great way to mitigate or eliminate cross-site scripting (XSS) vulnerabilities. By implementing an effective Content-Security-Policy, the inclusion of inline scripts is prevented, and only scripts from trusted sources in separate files are allowed. + +## Basic usage + +Marten's Content-Security-Policy mechanism involves using a dedicated middleware: the [Content-Security-Policy middleware](../handlers-and-http/reference/middlewares.md#content-security-policy-middleware). To ensure that your project is using this middleware, you can add the [`Marten::Middleware::ContentSecurityPolicy`](pathname:///api/0.5/Marten/Middleware/ContentSecurityPolicy.html) class to the [`middleware`](../development/reference/settings.md#middleware) setting as follows: + +```crystal title="config/settings/base.cr" +Marten.configure do |config| + config.middleware = [ + // highlight-next-line + Marten::Middleware::ContentSecurityPolicy, + # Other middlewares... + Marten::Middleware::Session, + Marten::Middleware::Flash, + Marten::Middleware::I18n, + ] +end +``` + +The [Content-Security-Policy middleware](../handlers-and-http/reference/middlewares.md#content-security-policy-middleware) guarantees the presence of the Content-Security-Policy header in the response's headers. By default, the middleware will include a Content-Security-Policy header that corresponds to the policy defined in the [`content_security_policy`](../development/reference/settings.md#content-security-policy-settings) settings. However, if a [`Marten::HTTP::ContentSecurityPolicy`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html) object is explicitly assigned to the request object, it will take precedence over the default policy and be used instead. + +When enabling the [Content-Security-Policy middleware](../handlers-and-http/reference/middlewares.md#content-security-policy-middleware), it is recommended to define a default Content-Security-Policy by leveraging the [`content_security_policy`](../development/reference/settings.md#content-security-policy-settings) settings. For example: + +```crystal title="config/settings/base.cr" +Marten.configure do |config| + config.content_security_policy.default_policy.default_src = [:self, "example.com"] + config.content_security_policy.default_policy.script_src = [:self, :https] +end +``` + +## Disabling the CSP header in specific handlers + +You can decide to disable or enable the use of the Content-Security-Policy header on a per-[handler](../handlers-and-http.mdx) basis. To do so, you can simply make use of the [`#exempt_from_content_security_policy`](pathname:///api/0.5/Marten/Handlers/ContentSecurityPolicy/ClassMethods.html#exempt_from_content_security_policy(exempt:Bool):Nil-instance-method) class method, which takes a single boolean as argument: + +```crystal +class ProtectedHandler < Marten::Handler + exempt_from_content_security_policy false + + # [...] +end + +class UnprotectedHandler < Marten::Handler + exempt_from_content_security_policy true + + # [...] +end +``` + +## Overriding the CSP header in specific handlers + +Sometimes you may also need to override the content of the Content-Security-Policy header on a per-[handler](../handlers-and-http.mdx) basis. To do so, you can make use of the [`#content_security_policy`](pathname:///api/0.5/Marten/Handlers/ContentSecurityPolicy/ClassMethods.html#content_security_policy(%26content_security_policy_block%3AHTTP%3A%3AContentSecurityPolicy->)-instance-method) class method, which yields a [`Marten::HTTP::ContentSecurityPolicy`](pathname:///api/0.5/Marten/HTTP/ContentSecurityPolicy.html) object that you can configure (by adding/modifying/removing CSP directives) for the handler at hand. For example: + +```crystal +class ProtectedHandler < Marten::Handler + content_security_policy do |csp| + csp.default_src = {:self, "example.com"} + end + + # [...] +end +``` + +## Using a CSP nonce + +CSP nonces serve as a valuable tool to enable the execution or rendering of specific elements, such as inline script or style tags, by the browser. When a tag contains the correct nonce value in a `nonce` attribute, the browser grants permission for its execution or rendering, while blocking others that lack the expected nonce value. + +You can configure Marten so that it automatically adds a nonce to an explicit set of Content-Security-Policy directives. This can be achieved by specifying the list of intended CSP directives in the [`content_security_policy.nonce_directives`](../development/reference/settings.md#nonce_directives) setting. For example: + +```crystal title="config/settings/base.cr" +Marten.configure do |config| + config.content_security_policy.nonce_directives = ["script-src", "style-src"] +end +``` + +For example, if this setting is set to `["script-src", "style-src"]`, a `nonce-` value will be added to the `script-src` and `style-src` directives in the Content-Security-Policy header value. The nonce is a randomly generated Base64 value (generated through the use of [`Random::Secure#urlsafe_base64`](https://crystal-lang.org/api/Random.html#urlsafe_base64(n:Int=16,padding=false):String-instance-method)). + +To make the browser do anything with the nonce value, you will need to include it in the attributes of the tags that you wish to mark as safe. In this light, you can use the [`Marten::HTTP::Request#content_security_policy_nonce`](pathname:///api/0.5/Marten/HTTP/Request.html#content_security_policy_nonce-instance-method) method, which returns the CSP nonce value for the current request. This method can also be called from within [templates](../templates.mdx), making it easy to generate `script` or `style` tags containing the right `nonce` attribute: + +```html + +``` diff --git a/docs/versioned_docs/version-0.2/security/csrf.md b/docs/versioned_docs/version-0.5/security/csrf.md similarity index 84% rename from docs/versioned_docs/version-0.2/security/csrf.md rename to docs/versioned_docs/version-0.5/security/csrf.md index 016741dbb..c8eb2d0e7 100644 --- a/docs/versioned_docs/version-0.2/security/csrf.md +++ b/docs/versioned_docs/version-0.5/security/csrf.md @@ -14,7 +14,7 @@ Cross-Site Request Forgery (CSRF) attacks generally involve a malicious website The CSRF protection ignores safe HTTP requests. As such, you should ensure that those are side effect free. ::: -The CSRF protection provided by Marten is based on the verification of a token that must be provided for each unsafe HTTP request. This token is stored in the client: Marten sends a token cookie with every HTTP response when the token value is requested in handlers ([`#get_csrf_token`](pathname:///api/0.2/Marten/Handlers/RequestForgeryProtection.html#get_csrf_token-instance-method) method) or templates (eg. through the use of the [`csrf_token`](../templates/reference/tags.md#csrf_token) tag). It should be noted that the actual value of the token cookie changes every time an HTTP response is returned to the client: this is because the actual secret token is scrambled using a mask that changes for every request where the CSRF token is requested and used. +The CSRF protection provided by Marten is based on the verification of a token that must be provided for each unsafe HTTP request. This token is stored in the client: Marten sends a token cookie with every HTTP response when the token value is requested in handlers ([`#get_csrf_token`](pathname:///api/0.5/Marten/Handlers/RequestForgeryProtection.html#get_csrf_token-instance-method) method) or templates (eg. through the use of the [`csrf_input`](../templates/reference/tags.md#csrf_input) or [`csrf_token`](../templates/reference/tags.md#csrf_token) template tags). It should be noted that the actual value of the token cookie changes every time an HTTP response is returned to the client: this is because the actual secret token is scrambled using a mask that changes for every request where the CSRF token is requested and used. The token value must be specified when submitting unsafe HTTP requests: this can be done either in the data itself (by specifying a `csrftoken` input) or by using a specific header (X-CSRF-Token). When receiving this value, Marten compares it to the token cookie value: if the tokens are not valid, or if there is a mismatch, then this means that the request is malicious and that it must be rejected (which will result in a 403 error). @@ -23,7 +23,7 @@ Finally, it should be noted that a few additional checks can be performed in add * in order to protect against cross-subdomain attacks, the HTTP request host will be verified in order to ensure that it is either part of the allowed hosts ([`allowed_hosts`](../development/reference/settings.md#allowed_hosts) setting) or that the value of the Origin header matches the configured trusted origins ([`csrf.trusted_origins`](../development/reference/settings.md#trusted_origins) setting) * the Referer header will also be checked for HTTPS requests (if the Origin header is not set) in order to prevent subdomains to perform unsafe HTTP requests on the protected web applications (unless those subdomains are explicitly allowed as part of the [`csrf.trusted_origins`](../development/reference/settings.md#trusted_origins) setting) -The Cross-Site Request Forgery protection provided by Marten happens at the handler level automatically. This protection is implemented in the [`Marten::Handlers::RequestForgeryProtection`](pathname:///api/0.2/Marten/Handlers/RequestForgeryProtection.html) module. +The Cross-Site Request Forgery protection provided by Marten happens at the handler level automatically. This protection is implemented in the [`Marten::Handlers::RequestForgeryProtection`](pathname:///api/0.5/Marten/Handlers/RequestForgeryProtection.html) module. ## Basic usage @@ -41,10 +41,24 @@ Then all you need to do is to ensure that you include the CSRF token when submit ### Using CSRF protection with forms -If you need to embed the CSRF token into a form that is generated by a [template](../templates.mdx), then you can make use of the [`csrf_token`](../templates/reference/tags.md#csrf_token) template tag in order to define a hidden `csrftoken` input. +If you need to embed the CSRF token into a form that is generated by a [template](../templates.mdx), then you can make use of the [`csrf_input`](../templates/reference/tags.md#csrf_input) template tag in order to ensure that a hidden `csrftoken` input containing the CSRF token is present in the form. For example: +```html +
+ {% csrf_input %} + + + +
+ +
+
+``` + +Alternatively, you can use the [`csrf_token`](../templates/reference/tags.md#csrf_token) template tag to insert the raw value of the CSRF token directly into your templates. This approach is particularly useful if you need to manually create a hidden CSRF form input. For example: + ```html
@@ -99,7 +113,7 @@ The CSRF protection is enabled by default and can be configured through the use ## Enabling or disabling the protection on a per-handler basis -Regardless of the value of the [`csrf.protection_enabled`](../development/reference/settings.md#protection_enabled) setting, it is possible to enable or disable the CSRF protection on a per-handler basis. This can be achieved through the use of the [`#protect_from_forgery`](pathname:///api/0.2/Marten/Handlers/RequestForgeryProtection/ClassMethods.html#protect_from_forgery(protect%3ABool)%3ANil-instance-method) class method, which takes a single boolean as arguments: +Regardless of the value of the [`csrf.protection_enabled`](../development/reference/settings.md#protection_enabled) setting, it is possible to enable or disable the CSRF protection on a per-handler basis. This can be achieved through the use of the [`#protect_from_forgery`](pathname:///api/0.5/Marten/Handlers/RequestForgeryProtection/ClassMethods.html#protect_from_forgery(protect%3ABool)%3ANil-instance-method) class method, which takes a single boolean as arguments: ```crystal class ProtectedHandler < Marten::Handler diff --git a/docs/versioned_docs/version-0.2/security/introduction.md b/docs/versioned_docs/version-0.5/security/introduction.md similarity index 84% rename from docs/versioned_docs/version-0.2/security/introduction.md rename to docs/versioned_docs/version-0.5/security/introduction.md index fba2706ac..190e399c2 100644 --- a/docs/versioned_docs/version-0.2/security/introduction.md +++ b/docs/versioned_docs/version-0.5/security/introduction.md @@ -47,3 +47,11 @@ Marten implements a protection mechanism against this type of attack by validati SQL injection attacks happen when a malicious user is able to execute arbitrary SQL queries on a database, which usually occurs when submitting input data to a web application. This can lead to database records being leaked and/or altered. The [query sets](../models-and-databases/queries.md) API provided by Marten generates SQL code by using query parameterization. This means that the actual code of a query is defined separately from its parameters, which ensures that any user-provided parameter is escaped by the considered database driver before the query is executed. + +## Content Security Policy + +The [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) header is a collection of guidelines that the browser follows to allow specific sources for scripts, styles, embedded content, and more. It ensures that only these approved sources are allowed while blocking all other sources. + +Marten comes with a built-in [Content Security Policy mechanism](./content-security-policy.md), that involves using a dedicated middleware (the [Content-Security-Policy middleware](../handlers-and-http/reference/middlewares.md#content-security-policy-middleware)). This middleware guarantees the presence of the Content-Security-Policy header in the response's headers. + +You can learn about the Content-Security-Policy header and how to configure it in the [dedicated documentation](./content-security-policy.md). diff --git a/docs/versioned_docs/version-0.2/static/img/getting-started/tutorial/marten_welcome_page.png b/docs/versioned_docs/version-0.5/static/img/getting-started/tutorial/marten_welcome_page.png similarity index 100% rename from docs/versioned_docs/version-0.2/static/img/getting-started/tutorial/marten_welcome_page.png rename to docs/versioned_docs/version-0.5/static/img/getting-started/tutorial/marten_welcome_page.png diff --git a/docs/versioned_docs/version-0.5/static/img/prologue/logo.png b/docs/versioned_docs/version-0.5/static/img/prologue/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d69865aeeb08e478c7c33821f5dd9c645dea4cf5 GIT binary patch literal 16906 zcmb_@Wmr|)7U%{96eL8Dkdly2L8Nh$8>G7%B$e(^+>~@TA}u8#T_Q?%H%fPRzPUN~ zoOAE{`<~y2e5|$RoTF=w5u_j|iG@Le0f9iUU{YdA5D3yC1cFR_7Y#g7SnQDj5Bw}d zMHOJ8qLgp#-kd&d?^jN11HwyO?9!IJw)-A*5WYe?6`R-0 zdHiS(ZSOEA+hLY2tgI}BPKb{D+3NeB;X3L%-U~YR8ov`2siV{A!tg$3v#Ql5#A3y| z2ET|?=k6pdV_o5WW2aW&G}=h#V_5yEh}j`^88=GfCmv0pf`TNfsz%_@mq;Tj*^7HP zH=h>b+p{AIkR`%Uhd)7`FMs>&)@hf&;QjLy{!rxiuS#JF-`}kLmgC1Kr8ZS^keg?3 zvz;z0p0Xdn+%q$Oh50tx{GPAPWbpXDgw4L2)#bk_<(&jAy<49?z_a9gt^LWRXRD|o zd0>bVr!1IQlXwG8V-{Hqs98juHYBd#U0&xi#bBsI!!Mv)AV! zNv&D7n6oSq(2soY-kp>Wy|z-wbscDB*57?uG|TQZb7rvM;@(-nh}#-xjI&0cw|18G zo|t)2_u&_=dq6+5?fG+7#>y|MX3IiN!gs=u2xAAj8=mcq@p|;8{VdJyoAwYs5O7%W zrbPhz|K*?W4L5;2Z*jhBryUIv&2!N^B1V?148$`F$J4Jv5*a!oM}ps4E{@vCs6CIs z7Rf-_eW9(l&1Ey=naG{Q#<1gXb0lZgLsql!l$m>J$#4*v-Vs7RtiX3x<1)Q{vUro0y8r^90!XF_*!;8Q#5lv;L~l!PPo0KEL!QP~k4fbO5G z%eE!~Ye735`w*gH@jg|!Pp_;tRs{)zOe933gDVDK$E2`Ea`2_%xm8tXo_FRAXuyYssRA^7SUVxy&Uw%dkN-0GQ(F+oRz<4Bo1)JcX!d!^sed z<#!_A8@vw(YeTbe6jmW<7DL^MYiqh&V;WX(B4Y>^e7xf;oP&@pT$Wh0M|%s95Z{zB zvawbxglW-T0d8-GEpC!eM8g5gaTfle`<7`R1dWxgz>d#tKYv!3A$spI6N%S zR&WQrJvN*=TysNud<_r5pg|QlHl!C?;1m5{sIGn zLAG0D1BVA>XN{k!ElYNIWt28y2J{>mTUm`cs6g=fem~X@4(5Cx=HZ9NGs-n^NR%JU zQ3QcwyUvmu%MwcQC5RngdrL~VEH26y0{LOJL#$!I`lT!%XU9n6^^xm*QCz)}x#`O` z!)FM+BjvTYo(h*-D@*tQ0Mj_M8zs!D__4C`!2`rX+>58Rr}M-99GTJ~1F2DDFRA>> zWCkyfi+596)QZ-rumLVc<+t2xRgvOL;reVDU0TbXTYD^o*xaC->8MU1P?OL({=svC<|cWA8Za3Qrb8 z0Qp;yqhG%yd0SIWP1&Ae$-9r=ODNmS?wX^)03bJNEM{jXwr`jZv&)^DPjso-Qh_afDT=4X#hcNLWr(eU z8+7*gOe%a4z#x^Bbb1KW>C>;>9sKSMkOHD19x5*0ra@<4V$vu>vii)5+=)y(PG9qa z#1If_KCdRr#Btoevq*)J0WQW4DK=3H@cWY&+Q3ak#kSlt95z!v?~`0ck;o71*gA%oGPVVi^*!03`m z5uEnfU}~|W1*K)EINmWc%aG9jLzqFSTw&bu;mA|PkS4IRR4 z{L`sIWPxY#T~x19!qhFcLLL_ttY{J!@_Mgo^4wVVkDXPE^x0bVmoHdifS}_oIf>7P zhUz9u3=0`Nvxcx3GN=TCVOKn+U&=<$cy3p1P0Rc_X`5G!6 zaiVyX932*o?+RHa@kDGzA09Z020mKA+P58)HEWh#@KZyX<`Gk(zBZks=xmwlR3HjI zAKrm#@_16x7((Javk|NbpK{3LFw4nO!9&6a zmIwG1wbpO%p#dus`}VmO8_QjV1D-H98f4FAY^$EszkSL)5b-_>`c(Hyom}W(P!o(*kl;`L|061n17bFU=7d{%fWzX7FMjBq?1NKh; zkB5l78}SxerbS%V%d%i)NfP7-m3%*8T(~9xJEK9Ssh{O2;!pCi7%dFKMTdvo=~v*~ zdZoz{wW%(JBE|QnhV$7$LEKFcJ-y?pin8G!F`i%3EE8R>MenkzM!B~`E;|V*@KE8{ z>ElW&F%KN49|L)Wq>mV9F-|SaCe;~GWu}uj7bi5YjKnRvVTW*<^h-B=$0cdiFma!n}~d7pY8Q8L5qXEHpp=Ebs-r3tjF7Or7N=sEja8}^2FZI>Gp~T8h*4_?6a7~j#Mv8bxDo!4t z!o!{nYN$AdU{zz9Itf`oh>U!ki-6r}m{@4^dgG?zJ(twA2dpgUY*yaHIa-uu2%FvGPXHO)#HZj_uT+sk~h(Hxdn!?(s`Y zJ5F8q~DWx$MQdFcXkB{&EV-heXlH{v!#po;}L$nx#ChtGafM`fM zgD%A%h;ZnD5t9SG1w;yu;&DN8d4bgqPZTXS#>^bx!XY;*)kl+_Dj14LlzVgOcF5*B zK_4zEe48J{1olc~Co900D*1wadH>rA`9V*vA84!KTeDZj0GPgxjjlzEAh&)~T1~qR ze1JcKwMC&pHpkFM$9;Nktq*>!IakapL9Njm^}!P<6v+9~gR$3U-lkhtKqT5^9bg@+ zU$J6=;y}=T0S>)ozbr=tv_Ndhp8@7I$SAP}K}1wGxmvqTUopYS9|B|DS2_rV(Sd{R z2e}lERJe^)+=v3I43ES=A_Y{Q0mSu9a2tP7`_x=>OBLl}|Sy zNbCVBv0GnZn0@J}qdI~Hgw17&%fZauUHyW4bW)qfH1aOw^arce79Q&0cBgh_VJcMK z9^n-_IX;>=5v`y>PVtPjqHFp$atTvROc-fWn`=OJ^T)02wS=?jRyn>q-s^_-{mnF~ zbsGde9~Py}oH)ZQWRsvbQyR)10a=lx2at&VtXskZU~AAFmdzpI#(VEz8I z3CJoKVi$-Ylnl%T1Wz`mIU93wBB3Yj|D9Ka8xuml67m;vyDWz#e;tmA4`6T%|Jw%u zNFj$kQ=^lKZX-@ON~`4kDtt?nW3q{~6CI?<5MLyX4H6d#@1OMk3;jN#tyq5~3c`xt2hoC)K+NF*;k1fHwgXTi| z011t8%SvB;bl4(_W8L%giPPnYk96eSW{8)b60fxI&JOn_g*PQ}W~D0A@YN58MPv*z zJNg3=k*JvKlNF}Z?97qM&uWqW?#Rg4!%N0xvE11r&D7zOSLvp@ysDf^CkHWnXm4ME zV^o$=#p}8}PwHAkafb4eeTog|g#03WuhK;6tPYE+ymGpQbxX~4IaDp?Uc^Rx#~>5+ zdD6+4@RZlXhwZgLmhy6TG=aeJi;A0N?};#MD2j7B#aicOd^-KsLt#1ZZFD8-Y5 zK0Fd&s2jV5n}Tu3of9V2Mi~xg>pq~fRt8XH6f&S-^V)*DC#cH~n zyzO_)m*wV!v+X3p2igIdDd8=5f);_8p3b6no^GF9I9)5OgHR=ibFPOd zJKd}JeE`LPM~lvtGqU*L-Ni+E2kDIWC|8y%x!JZMwdo%IBBeJi@Vj5A# z?U3T|s$|+W=&r4#$mB_dIjnqxkvLhMeed$@Q);@D&Q5(Eq|l5(=0|_P0QX}(nE7~Q zpwPnBDAt@ArZ!cw6TS7C&V+_EIxVByr$)sh*ONL}hD}m4x+I3iNw`az3`8s%%O28K z>o5|p(+8GKZ?fHIJ^j8$vcH$4$Sh>a=_D59JNG%;R^nOrKDPMLN{I7-N`x6O%~x>lUa82`)qeE9ePuq!!m7axt&d~Ym_LS2gUMfWGp!N%bv$^(&;~_g3frgOz+X< zT{XtqYiqeG{+@|dFhjh`7=GA<*F^Dy$W*RT2?K59=+wBL-T;nmG0xLOHpV%pyl&TK zHOx}QboAk&o8fO*vvJr+p7xuMZxVMj*W-1*7@DX2!bHIkT*L9@nCPtEw3oMW;@8w9 zQM7m3!E$J(WWy6^zT-!7y&%K2pOk1X5w_otV;1AqA-@>Xu`DiaZ(T#j>mnIV6&i}M zxa_`xesSnuV;r{CU}$^rRFCWQPvCDZJi z85aSMCuD9dO_pOm*K(=hLR6X?b)KmM#$20+&tmM`GlD(qHUIP^6P&q5|Cp+TMM}Nr z))!$|Xk}tI{H*l!m4d+!G5DBIUNId-{M7Vji)oGG`NRtS0=n@2p{-&c6WiJTGz1<- zzu~_~#O^g<;=8Zd zI0@Ct36?Z1f84h#D~oYEiu{uRV?>*!oQ4x!%w^spLD335<{( zmd#mYim@t=PaciUyvrJ>;k;^*=k88|M;~g_+y~3UZ)63xT}}0b#CzK#(}aDf0dsu5 zMPd?zxOjiR^1{59w*17uW_u+~@uI0HZfpCvHOO)INkhS&vRo`o=J9m$`;hyn;V+b$ zslzVkEw3{}G2TpkZm%rDD7jIv)z%)Ki^zJF@ag;bo)6QmA9fr}HgvM-L3`KGF2%JJ zG2x}3yPl7#?gqKqY70*NoyG=dIczCoBqNm2A?-I(+1Ey9#sk%ifpyRo(7T#bd#bMrtNc ziBu-XbM30RK;`Wiy~t zeD0Q^vpN;NlA2*_$m?~&VJfq+>b8F+d_ih@xl1+ILtsz`49;;U+thZtevO&u4HPw8 zK-#8B>xs)s?~}UKouj8!FP=3U9AXG2klf7C35vEfdinc%Z8*Yb-RfLMuH)lr$H=05 z`iME8iShD^>wEggE4*FK{Pd2Sp>vfpC-?&AM4{JbWR^Eo`UMy8l}v5+=%*Ir6Dtf6 z(F+aw0>SAvl1Dqit&j8MZ9hMr9V{%SOIH{(4wpDedpBH?mQ~or8c&?IHs=tBApLmq zZYx$W*Ym@jZBrbMwKW8-^4ome`gyuh_xHIs3(L7Tr_5c?FWLs`HG5q1`%iSl!%4G4 zcJ%W?c0APq5kB?ac)V7KK69Qsbv~|X6w;@bm9?(&gSIsu)$f{Mx2x+S3}7RHtcXII$i<(QG2cTo3>n1 z`@FIoe8!0Mog0UvY{GF{kQRAuc%RYE5!TY6+S|wMU15%tWzG8%3PvY=`KCQK1UeOq0Gc|nH-m>n!orp3=_9n#tW&!8n=Wg2ki`e)YgLeR4 zI8WipQRPK0bf+M!;2o^#%DJHVwHmF>Mmb3?u`Jp(c!uRkj`Cuw8)%G$C^gcQ;1Eq!eahwpR-l$?bN&C=7$ z16nMLiWET*Bh9~NbuA$2hLEB|xPH}4oxZ!G4U?7dv*zwQe$rRx9i68a2RN73PQs7e z*?EOUcRtRfm-BGtZS-xpr<2{R@lgCQtQb*~iPz5u6h2!&ncoJ&vpOd|WWg>ev8ssQ-ZQfMkxj7`ev?;ho~E{PBRI^@KkYQ) zJjbXxW;veq=6q$^1fezOsu4ny&O7ew_bN&n^~=;4(8&scQ%b~54?nR`?TPc4^Jy|n z_w4m<)m-;JB5)GA^g)=Rt&@@{_ge1F?(!=;+cE@UuKvW^oD|cMD_^|v$sB$trve-S z_EkiT80#U8E6Q-4367^3{i~JCjp1vGrr6MGY2mghoMgi~vWo>=q~q+ig*k^U_jwLD zkImp}`dr2XT6H?zM9I1j553@xV+1|T0rVTe`6OO#y7t;hBS+(zvqE$ew&+Zb_dmo} z&Dh9?56TIN1FQ9{Mn7x#+v=5W^Ke!uM?`eNAK}YL>lN0CH1fX2nm-d;2JkSXGsmf! zXr`fImBho4ET7QNXVs%7IvGAs{0{H;Eq`w@5y>!CaCfcGs=}mCOj}CZT%nrU0u>P= z>peFwc(3j@%z@S31@QI01due~Rqy^Npg+v|4Seei;ybC)W80(VWTwN{-XqLX86M6~ z*HoEGAg3oGd{{ZtpuJkyAH6_&Oa9OAY+NwN_F529A{Jh;?gm$_r4x5ZPGNC*Q``#> zgoownshpCg67bDCZtt0q;PZW^uLq0XJaWgUV1Bshv8uNK&PBjX-8t@j@;W0|d~e4H zzFjlIdkZuTAz|f=peq``5+(a-6CG{#$Ufx+TU@mYCre+EX4x38~v3P#EVc&l5 zIPNSz4K=Tu@hEVpoO+pIEXBrYUFT8K?>9j2Xd8GiThp3suCPi31rKqV4nN$2eqRva zRRs-~AXZ+T)k-Z#2n0vX_}8uNB53%JleQX$UaJ+NH%Wj7Xb5*c(_HC6Rn$LWe}auT zgW9>!6crqmr?!GHW0J4f_`vRxT$Nqmi~gEl$)=#H@uYG_GlTm4))=BZR7KyHS(DwluW=$(Wr~v`?F%J zfQG*VIzAs|=@$7IfxF7Ky*zdMi(B_qA7FEGO2DVReXaW!{nF}RlsXaRLc?2ix2MLvwKM^A3dP;S) zFY^KSK!V3_l9MMt-_JwT7b%SiLFIq|5V^HidI<1&;c71n(6S!8S51vQyaQGdO>OUS zBq*7&%WSIOawLF_RSh6$1@FbOvwIF_PcFbZT`jj05&!~u&B7- z&rdVsnFWjU%AMQqL>s2I`R`IaLOam4S6k)yvpwB`!0$AsV%;0A>i$Y?)lb9pfY?1m zZ6!n+6yt<3ua0D)fv^k*<&>2^c-0F+k&Ko$|5Uq2Pkw&xN9RvL74>0F@nU@fYL3F$ zsh+_~P5Y|v1L4gAr2c*V##BdLbo5(`%6AbZ-%2A=AsvwWe4sRn<iR&SUH2)FT`#{r-17?>wDgLcw3~~cNTedaT5vM5RB=r@d%5$~rhL1p0ws2rw^Dcz+VHWqtYcPpFE(brsQXxXY_b zPS#V&_n>_IMw)j~A^Tv2^oD@XrKU~%yk>;~B+-Zi>z!$MJL?_G7T-#hLM7+k^K@Qz zm@v@7nE{mioV_<`O4xh}l=G(SJe3?Nw6=)gy>-qj0D;6~4S4!7z9@-G72eH9BR$7y-8t z+krl7EUb_6Zb{)F`dedCidfgxqsMW@-RU|KxInYCUnw0JJh^YMk+S9d#*peU*thaU zLY72^aXWh?7TCZaW=YN+vE_hP?z;22TkJW27-r563nk$5Cw({pY|~Mqij^M-6nvV^yfh7eNTq_-D8f2*o9$7l7~h zCr!m2hC?JToK)xu|I;!MtkFqAqKBj05JoO&_s1>ie$sGbfKuHZ#QAB9$Zlt^lw@dL z3W>^_-9Gu&#hcmP0iNZ>a}jOMRpEz3!eCFT#oVrxm13NT=TtKKBOTr8^2x+;?;Sra z5=Ncb;Q@8Rn3iw@s>krKEcb!LthnlPj!*EuzK0JVPJoM_2T${9cc~hLX+iaFnd7_I zb6~39!7T52bL_MUKJY{rwBv{jg-U+b-%QME#qj!6;C70AyYOrGNAK>xz>WLpVV1*r z9Ta2@ZVKdfXewZLB3I9q2R%(W{$M5lMC^I5%F;NBgvn8Dwd1eP?nV#OJ7Ne1vF*Py z$?T6L0^Bk|2+=s^(=x@?-K&Bwnu@HJlm5~%mp6zn{#XIDe{4LL4{i}+2M`6wbHG?& zm<__(+xYH-K+@pYbGjpmK#V0%`lw!p;oNRR2(T;lwypt|%-#s`O;i0Ru+KJu2BS;O z!uN!BiHsI>aP%g;_oROyU@Q%RLOl?b)xIyuPBcOqXa!X6{?=;+=5=}y)x&3>v2Z#ldo*XbN zKTW4{Xj>ZMd}`Yg(&*u@@lo4jW24nwWFF+Wh*KXBzEW!Wt-k7M7Tkw`Jd6m?@7U|@ zo%2eop)uirtQG`WIkev+cGGt|IO9Ebv%cNm8UQpvZw18l_7l-7Bk+kA;~~nECm9s`&du&Gj^B`g@A9wK|80swb&bU4}l@h)BTi~<^P(H zkIC_HU{{9K6amj`!ze^Su88Iq3BH6Z|7!{^uW`BD{sEj@1mLlcqKDaOm>jq5C4~_} z)JACnQpuu3Wq0lKPuMa^H?o@``(28|&eVs$D)^=)1T2nogh^T)(nZ;#Ow(oICfoZc2BEAJN zS>gD$E!fm@jN*n;Q-*R?UDJUAO{nBxz{yg#QmnEdn%y(FuitBCU9E2Px{6A>ivD1q zp|H091iGTpoEsncR%5GRNBN|@4;Kv{=87l?iLY;Pe_mwd&1}P!D$DF2khGQgs8tZh zSZ^q3NCN?uPt0{l(lU)-`%mcOyVMivtM|=c>K79y^W?kD&cyHnZ`e3xtJWoWFno1c z{vom(-a9t>76RhJytp%m#qIHzL_jw>{h^lH6;QIK8#10Q#Z|xaXHq{gKd7#V;(RHp zl&H)N5Wu^ioEEF9sJ3q8x`1Had&%?%sOdGWxh-&4 zFelKtpwBUw!moLO=^@uPU0Mvvxl5$~Wq2 zFC$;^@pRE{QVDXaR%RNf-;m)|ch%L6RF0 z^)aSE<#i%k(YX5>k~wqxBUR|UubjF2(lh({-#nCfU)+D9d+ce-@&GR6s?Ty@hLC#% ze9tR0MOi))e%a^?$CdaDGli$U0`)H_3&BadjTLnWO{@88N3YY!_0CPsZW{&DK~%cb zJ4hnUDMFj*M$e}9^qbTHAELm0zUMynO-CDng(XP0jS@rdBky@+?3ilhpp)c2)~fW5ifa5OfXyi{pJ8*)c4LwWU@&>~O$V0t2km)`MhhSWO3qt)*LAzL)4 z;~UYci0NCpwD!sFF?ck$?!IqV!K}+%XGLiE8}sx&na4`)`svjUetyYLgJzhy4_z-t zYBO`A0L9eUCI6fh5;az}Jq>Xm0stzL^(?=1!&xo|;;nYC!=+cP5Gi3Cias1e? zPf(aDJzo)dGaO#cEcN=CdQz8p)g`U$LFhp=vJB5Iec5whb|3|7LZlRbF`!o|%bOpx zdq_G?0L@Zk8!8sQe#c(lh={Sq_$YL9M@?l+`1yfx0EApMFVNo7x|j>Lp8C?=mHX9e zV+1<4<7evHBjDixmtH$w)g>xt>dfSd#FHZxs2TNQyx&?`)PITK*v8Pdh)cY zLDz;m7Ng;W)zCm>+`i2hQMz58aAByoLosx(i!<|kplPE%|6+-d zAb+kHB>!E+&-_K^O?`M8_j*25BXWMNnivmxTqjTeusqB+vsue-E=={_f`(s#Us`F+ z2_F`4d2R|a3~w#?x4flUH*l{&{Nq!9#%zguC36Ck%L?|7?-2%FWTLJ9EtVG}7@*KF zy!-JwF?q>IbM?Y2jvr1w^|0A;?51$b>jxE2(>rKDx|2|u9H{J?61<8gj+3M0;4N3C zX^Yf3DZg=}?7#2{l#O?BDX>vZb-&)nFf?c{T?$KBbz5)(Hljf4;ao-N1fv(5_C{f^ zaobFdGRsMCE`q3gAB3-~Pb@Tc8byir7#)R#!-M(*DDvETbrDr!b`Vo@OaS}MoUbr8 zB#(y%Pm8G4cah8PwP^M_&;Y>>nFrOdxvRx+)|bsH)m>YV;Fw)KMq9 zIr4B4I-~?GZ<6+FK;&!qlVVggXrj|zQWo}mbvZi$b`^o@cPu+D4|6E4x}cAja*u-< zLz&lOpx2G~7Oxc!>TmUlQc1sL`OE>|{etC3pjpO_sYfa_ytw~jr@(a9TD;Qs$$ilF zL<4TY5a5E^&RuG)W3^dux$G!-R;|VOsNLob^?g_5bdZ7|;M&s`ue4TNrWO8!MDM4U zk$Y@05{5*YJADa))zzs^x-y%BvS67-6SD{HBABUt{h7Vaj(HW8N#Oy%Vb(P*(a@cy zHuGrw+FY^qjgfj^4?)n`bk*c*f3o{~)5arJvI%<=Ygs$p^Re;cxmJ)a-4EPaSF$?d z6e*Dod@qi;@QPYFB7XwQa{ISL({yRL|aGhlb;RA<*xgF}pEP zrcwnpT!eX|{KAx-o2Fv2p=L@D-$~5zUIa~L)#A<&dS#|=5x44ny!Rzvm$3EErtk~G z?=FZOS2m<|#$CJvjMe~jRa^!QX&IyZ=I&|kdZxXH*XpbKI-$>Syl%jqm?+Chw`x`? zg6}A$2EbBl>E`SDT>6=?yxXh*1&0RVTT_tz(G+0{8h-+i?f%CUyhT4DR9g_}YLE|5 zd{KBja#C3Tc%Nko!8v(b_p+>InhOs?pLS<03p4?c%7`mXf1=(pVc=o2{`Rn-yN#_Z zBLZCLC;&3XAM$jSLg%KCj7HA-{VWT><;L2d4=Epcy`Y zdh{+ze6qJ!(icdP;Mk|IlP)#}CH*;%^67-lMeU4dDW*JCHJU}LGBs_;_d;1*XujZ*maGAsl{4N+rGZ;Xbbj_6_ZD8!g)5o>?afg zA3|X7`hYv+qtMI+bnIlLkAlg|Uy8f4=gH4CCeRRf8{N9rF!L)v0=|e<1Uq{$ql_VK zX)YCXEe4O!PQXD?uS~jfsX6E5cA@f9RyA~>{IK&(ix6}#hFDA1X+2_50aEsKo2_O= zck7#MZ{N4hmuE|xCP-dWmL_o9BgU= zXA<`UPCk8*Q~k51OK?!3WVyygc{%XxqFuf=2)YwK-8qRz?U58qlnNhz^y_j0qv1-; z=n{Ib8j90!QmnPyqi8avhOO~)5Hw|$+cR0o&>teT=YR1Ka@qX(hPZ%<#%9G8`fB$5 z&SZ8REy2CLnoVq>eX!7kmdyrMCr(mCY7b+#3}E8*&sjJ?^{3X>jy1t zOlyrc(R4Sg>;hZvFdmrn0((n@y{^;cwM#J9FbF4Pynw8v;Y9Bpogip|b6Lkn;q}4z zVJRM`lIi!`zS4z{=I9s8In|{nb8}Df>l>?GD~zW#)sBtAcK%Ha*lQCO+(U&X!rq~)b%qwNy5vL9N zHaP*9#IGn4UObMhUxVYHmfgxn4kin(wK6VTU&!?7?t*AYxKhN9q#>Mr)QCM4TC ze@ltb);y7Hx+ZD#R!U)D!&y-q9nJFJ9FWjrX6TL#dcwZ~xUE&8Qg?ldev?A&QP_FK zm6ClMp4VzrIuN3h`$sV)UQj<>T21L(a_C9o7WBS0#hY*Dq~2I}mP#7XP*Jf!*@kMS z_SpqzSU45vTHR>dDrfrNnCzP`hkBIt4_zKHyFA7&Sl~G+*htLf9c3T>r?)t0ACq=C zdfwjBayIU82GLPv`U;-a!fVEXUz_Hp^=E5}`*XEQZ{UO3tCxP)iIryjlL$-K@3CX= zYe;XysaCbDGBz9kx=90yZQfB-d?6~Q+*LZ7#M!}Kgd|FJ`3F5MecRNNGsX1ekiU~u z)I2_mf3og}zVuH{`Eh7GO}Uw@yx!5ixP1>t*_X&gdjaan>*qcfY_kG5p1WDqCQ0() z)n6sdWWPIw^%Q+!5w;%rQY>~Q{?Oag(pI)-bb5W#h}cVXP?>Zp@Oh*QbiAb#GB>m(?l zRY-o405mj7o}4W06mwBp}TdxXin-H5M*Vfn3>BPFsvv{+f zq`RP#QU7cCW~X9C?|0eO1Wt||0!aN+nu6HBTCM#1Z0QeCtP-AY$zY6o(CCVgKz`rD zK{qiE%l~3L_!K_t6G}VOTi%dfLLpBA!NoZSeWNmZrZXgE<2(U(kJ)nV&sum#c{{X} zpB9+mA^9d)#5+nlurJ){1l4twihsN05uJSoQq@tKTmD-|XB8Dwpna5rGc+6C)(M)Q zyaKo&lwbaBa0P98uSD&d;r88Bnq(3rM0r({{ElrN6km8rkC*z=w_nt*1&TVY=S9{h z5QX%#uE%YzPhNzYfIY)`J@$U(V;M`t5*hnEVfZKpTCFo8Y<*5pyuNnnixIg>^V+N(*s312?y}d?P0zPjhdSAqx=Cd1x?eMc6 zmc~=J@V@S08%gq<$Yy*!f%~roP5x>2H8ywz-)=TyOLH&d=%r%vistIqFV0d;u38nY zbiWY#bl_0#bo2zB;n&-H&A$JPp@0s)hO0_tPApnfLSm~&hyjRxeH`xX{wOR829t&( z#n04hok0sn3w8oz#3*I|=}V5b!6SihJ4`*5CnqOLb8g6bOuP4l`h2Nmi>&tG>d5lA z-P6g|yM^+9`kd(<=PVgh?ISjFx}!Et21S}?@tsdrk57d9TYHPEuSpjM2UWa>|L}cC zp6mCq(ghuECHZH*^z?@x2L2h}fuVSf5C^^94tRWIonjleu*`G+zk06~R_|Gaa}*uW zE=Arwr_$r;N=;8LD!}HIdTHw|I(HPtO~zxG4@~Yuba>isl8m&(NwpvkiLsfqH0^`K zU+E;thh137{+DLF0Cva+(+a8-k{YM!s93C@FEi*;{iE_FyRfFIjhp02RzGGmYO>DQ zIW9a1Sm#3QUi7iLx4V>^yW}A%JnfJ~CMVUr=n?Gr#o+gM_eXl{jfa$++7u<<9y~r@ z_p;jTc?Hev-{Y;!qaQc0&gVGBhB1eK)lV%hsG+b9ob0i0GsPZ5uvxL0^3S|T&%l3o zJAxM7m$j}9(P@aA$sBX)32z=Tdiwqa)Ys>Q zG3j9iUnWIBJGX!R_&+lTzYmU`%BNlI%bbofrZ4@y+$Tt4msCfE5N+Q^?b12<)o;tV zlb9@+gMJ^7TfZJ03{A!_jZ2>U+GGWbE<6)Qi~}WcTT#(KGpOZ=hJvr$K%4LbE2qIg z<{#fz{a4Z~#jyiTs+Gk*}36(u4LucMuZwfjhi5jHRoj&?}T3l)w{i-D@`#CK-Vi@%1@z zfwsdJs~u_#46>lh9IulEwHbzq2jI+Zf(B7FOyDIQu(d0`d=vpoyDS^>UZ;d;vt}X# z3D`ZlaHKx)wfiC5rB`YH5U@jP*61b)Ia0iAc`6I(wD;f17kbA}RpTDQ`ubFBxPuUZ zHy=yS)mxL^L)&IcaK*)m^N}u;TfZTvjaU@vT0@=Hwu1?55R)_)BPtp%24p#0o`KV1 zDk8rY6KiYNrGbf*O#R!j9$=o+rAb7l>y3(7x(bRtQot+Y@78WLL*JYZOh;d*(Ja3~ zF6BERvI6LdW;A}|8mbl?gVn;6NP-}u{-rsf1nOtR#wygwvCRx15S=%qGw3XfXZ48& zJ5fI+`H0!;6q?UI7!N}sqA74O?e4QLh%2GJfB%E%9iqIH^;)MIW4bh&Oe0t*7=p-a zPY&ha;m*xTmr4X7{d{ruCxch?uWhRaRp&X%MD&3dAXEHZTdJyhgg`HCP(#bLu%cF7 zZ6zA*tNg#s^dR??zgXlbBZ$JL!!qDOP%7YN{3Dy34bihNA2^D9onc9R8p>cU;KcH3 zJ%53~-Zow_qx!)^*m2`%yTI@2I%Qrg?wooA`f?pn>$?NPq7?V{5?FVo^V!@g=8LO*B~PD~~U< zrA5r%Fv?~tW82TfXhx;JBH1miD7G_SR4+b_Ky07S{fbRMhLa%b3d7>NErL zSgzk|eXS=FzSQyrf@?r_$gR6BE{Yh~%X&9n{`WLs1ZsUqu5?~2EjV=H5$wutef@3>#m_1;EB z=0`Z9Sx~yGi~TdoC`kkob0UN(`)dkP3dQg1gvLyiuC7N@B(vT3a8My^jMLUV2`Dzy zYOe?~2(rN|t3j=eS~*Q`*&cMnG>sG`Ra`-|Cc+{$Lz8cf5C0P zQBBL_mI=x-QWCe*P)^&ihk}E<*^*Sn%FiovmYqHW5&z`!s^G5)s>a<$?qjp38A$ki zyQu%=ov2|yqt%PBHCbNoVD#~VpqmvL2@Hy*6Ai8RoM=a#RX9O;7Fo0_LSG;z3(NgK ze=g~;V?y{HyQBBUGsF`kOleOLvx8`QoBz&;1?Go`j`V<@bnK9w3>2>j1c!PLAGCRK z=2WDL`Rl9zaSR8OW}tPO2rqOfSN>~ox7n+W2+hZ8o$_s}sy38}EsYd5QRm*8J|bS@ z8Vn3pFB}M|@0_FyL!Nuh?MGzh`G$~y+Jp;Y?z@Y{v7Pjmv8E@U0^l zt^Cc;f{JPked$maD@~gX=2v&Xh=bU>jaD_)5eF{B2Oj^<6ei<0ef`&Is8ax(J`5@7 zyv+#Fq+biXb49@OAFr`60_KS+O9@Nr+P-M0ck(YwzWUMoo-HK zTf+^%sn#}el$5*vsimy9y}u5G#J7*fR8di)-|kI>qhFR|_?4@m#FsL`?V=hcwQqZt zh%ZoHGcu>F<|Nb!a^*PL&=gv=j*b?ui~#Y0T>YD$gbs!8{yoR*$_<3*JLUL4Gk9RU zi{sqfMZ;>_q>uq=*819hzXs-=X46iY-dqqrd! zUjIz4B8IVXssG6(gsYo68tHh)mepAFWxn_d^NK-E)#aA<0~`t+qC$dcxsRLLMyx|> z@_7^NkWI0_LeYOCd4yOrNGtt@_)N!kTk=?!^pzCN$hiXr`ff}jgBc!M09QNj6}oOW z9_27~>x#R9e*=j4-ikAH@K5@sWOC>T%LJI%Ey h$jJYHo}!;$xIuZ3yK4nECNV>71{0SPD~1}p|35v`q*4F? literal 0 HcmV?d00001 diff --git a/docs/versioned_docs/version-0.2/templates.mdx b/docs/versioned_docs/version-0.5/templates.mdx similarity index 81% rename from docs/versioned_docs/version-0.2/templates.mdx rename to docs/versioned_docs/version-0.5/templates.mdx index 777778e82..7b04a9d12 100644 --- a/docs/versioned_docs/version-0.2/templates.mdx +++ b/docs/versioned_docs/version-0.5/templates.mdx @@ -26,6 +26,9 @@ Templates provide a convenient way to write HTML content that is rendered dynami
+
+ +
## Reference @@ -40,4 +43,7 @@ Templates provide a convenient way to write HTML content that is rendered dynami
+
+ +
diff --git a/docs/versioned_docs/version-0.2/templates/how-to/create-custom-context-producers.md b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-context-producers.md similarity index 85% rename from docs/versioned_docs/version-0.2/templates/how-to/create-custom-context-producers.md rename to docs/versioned_docs/version-0.5/templates/how-to/create-custom-context-producers.md index 3a5864b3b..52a28aff6 100644 --- a/docs/versioned_docs/version-0.2/templates/how-to/create-custom-context-producers.md +++ b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-context-producers.md @@ -8,7 +8,7 @@ Marten has built-in support for common [context producers](../reference/context- ## Defining a context producer -Defining a context producer involves creating a subclass of the [`Marten::Template::ContextProducer`](pathname:///api/0.2/Marten/Template/ContextProducer.html) abstract class. This abstract class requires that subclasses implement a single [`#produce`](pathname:///api/0.2/Marten/Template/ContextProducer.html#produce(request%3AHTTP%3A%3ARequest%3F%3Dnil)-instance-method) method: this method takes an optional request object as argument and must return either: +Defining a context producer involves creating a subclass of the [`Marten::Template::ContextProducer`](pathname:///api/0.5/Marten/Template/ContextProducer.html) abstract class. This abstract class requires that subclasses implement a single [`#produce`](pathname:///api/0.5/Marten/Template/ContextProducer.html#produce(request%3AHTTP%3A%3ARequest%3F%3Dnil)-instance-method) method: this method takes an optional request object as argument and must return either: * a hash or a named tuple containing the values to contribute to the template context * or `nil` if no values can be generated for the passed request @@ -25,4 +25,4 @@ end ## Activating context producers -As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#contextproducers) setting in order to be used by the Marten templates engine when initializing new context objects. +As mentioned in [Using context producers](../introduction.md#using-context-producers), context producers classes must be added to the [`templates.context_producers`](../../development/reference/settings.md#context_producers) setting in order to be used by the Marten templates engine when initializing new context objects. diff --git a/docs/versioned_docs/version-0.2/templates/how-to/create-custom-filters.md b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-filters.md similarity index 90% rename from docs/versioned_docs/version-0.2/templates/how-to/create-custom-filters.md rename to docs/versioned_docs/version-0.5/templates/how-to/create-custom-filters.md index d6342e5f8..4cd5d2f23 100644 --- a/docs/versioned_docs/version-0.2/templates/how-to/create-custom-filters.md +++ b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-filters.md @@ -8,7 +8,7 @@ Marten has built-in support for common [template filters](../reference/filters.m ## Defining a template filter -Filters are subclasses of the [`Marten::Template::Filter::Base`](pathname:///api/0.2/Marten/Template/Filter/Base.html) abstract class. They must implement a single `#apply` method: this method takes the value the filter should be applied to (a [`Marten::Template::Value`](pathname:///api/0.2/Marten/Template/Value.html) object wrapping _any_ of the object types supported by templates) and an optional argument that was specified to the filter. +Filters are subclasses of the [`Marten::Template::Filter::Base`](pathname:///api/0.5/Marten/Template/Filter/Base.html) abstract class. They must implement a single `#apply` method: this method takes the value the filter should be applied to (a [`Marten::Template::Value`](pathname:///api/0.5/Marten/Template/Value.html) object wrapping _any_ of the object types supported by templates) and an optional argument that was specified to the filter. For example, in the expression `{{ var|test:42 }}`, the `test` filter would be called with the value of the `var` variable and the filter argument `42`. @@ -22,7 +22,7 @@ class UnderscoreFilter < Marten::Template::Filter::Base end ``` -As you can see, the `#apply` method must return a [`Marten::Template::Value`](pathname:///api/0.2/Marten/Template/Value.html) object. +As you can see, the `#apply` method must return a [`Marten::Template::Value`](pathname:///api/0.5/Marten/Template/Value.html) object. Now let's try to write a `chomp` template filter that actually makes use of the specified argument. In this case, the argument will be used to define the suffix that should be removed from the end of the string representation of the incoming value: @@ -36,14 +36,14 @@ end ``` :::info -You should feel free to raise [`Marten::Template::Errors::InvalidSyntax`](pathname:///api/0.2/Marten/Template/Errors/InvalidSyntax.html) from a filter's `#apply` method: this is especially relevant if the input has an unexpected type or if an argument is missing. That being said, it should be noted that any exception raised from a template filter won't be handled by the template engine and will result in a server error (unless explicitly handled by the application itself). +You should feel free to raise [`Marten::Template::Errors::InvalidSyntax`](pathname:///api/0.5/Marten/Template/Errors/InvalidSyntax.html) from a filter's `#apply` method: this is especially relevant if the input has an unexpected type or if an argument is missing. That being said, it should be noted that any exception raised from a template filter won't be handled by the template engine and will result in a server error (unless explicitly handled by the application itself). ::: ### `Marten::Template::Value` objects -As highlighted previously, template filters mainly interact with [`Marten::Template::Value`](pathname:///api/0.2/Marten/Template/Value.html) objects: they take such objects as parameters (for the incoming value the filter should be applied to and for the optional filter parameter), and they must return such objects as well. +As highlighted previously, template filters mainly interact with [`Marten::Template::Value`](pathname:///api/0.5/Marten/Template/Value.html) objects: they take such objects as parameters (for the incoming value the filter should be applied to and for the optional filter parameter), and they must return such objects as well. -[`Marten::Template::Value`](pathname:///api/0.2/Marten/Template/Value.html) objects can be created from any supported object by using the `#from` method as follows: +[`Marten::Template::Value`](pathname:///api/0.5/Marten/Template/Value.html) objects can be created from any supported object by using the `#from` method as follows: ```crystal Marten::Template::Value.from("hello") @@ -51,7 +51,7 @@ Marten::Template::Value.from(42) Marten::Template::Value.from(true) ``` -These objects are essentially "wrappers" around a real value that is manipulated as part of a template's runtime, and they provide a common interface allowing to interact with these during the template rendering. Your filter implementation can perform checks on the incoming [`Marten::Template::Value`](pathname:///api/0.2/Marten/Template/Value.html) objects if necessary: eg. in order to verify that the underlying value is of the expected type. In this light, it is possible to make use of the `#raw` method to retrieve the real value that is wrapped by the [`Marten::Template::Value`](pathname:///api/0.2/Marten/Template/Value.html) object: +These objects are essentially "wrappers" around a real value that is manipulated as part of a template's runtime, and they provide a common interface allowing to interact with these during the template rendering. Your filter implementation can perform checks on the incoming [`Marten::Template::Value`](pathname:///api/0.5/Marten/Template/Value.html) objects if necessary: eg. in order to verify that the underlying value is of the expected type. In this light, it is possible to make use of the `#raw` method to retrieve the real value that is wrapped by the [`Marten::Template::Value`](pathname:///api/0.5/Marten/Template/Value.html) object: ```crystal value = Marten::Template::Value.from("hello") @@ -81,7 +81,7 @@ end To be able to use custom template filters, you must register them to Marten's global template filters registry. -To do so, you will have to call the [`Marten::Template::Filter#register`](pathname:///api/0.2/Marten/Template/Filter.html#register(filter_name%3AString|Symbol%2Cfilter_klass%3ABase.class)-class-method) method with the name of the filter you wish to use in templates, and the filter class. +To do so, you will have to call the [`Marten::Template::Filter#register`](pathname:///api/0.5/Marten/Template/Filter.html#register(filter_name%3AString|Symbol%2Cfilter_klass%3ABase.class)-class-method) method with the name of the filter you wish to use in templates, and the filter class. For example: diff --git a/docs/versioned_docs/version-0.5/templates/how-to/create-custom-loaders.md b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-loaders.md new file mode 100644 index 000000000..0286f6665 --- /dev/null +++ b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-loaders.md @@ -0,0 +1,29 @@ +--- +title: Create custom template loaders +sidebar_label: Create custom loaders +description: How to create custom template loaders. +--- + +Marten has built-in support for [common template loaders](../reference/loaders.md), but the framework also allows you to write your own template loader that you can leverage as part of your project's templates. + +## Defining a template loaders + +Template loaders are subclasses of the [`Marten::Template::Loader::Base`](pathname:///api/0.5/Marten/Template/Loader/Base.html) abstract class. They must implement a single `#get_template_source` method: this method returns the raw content of a template from a provided template name. + +For example, rendering the template `content.html` with a file system loader initialised with `Marten::Template::Loader::FileSystem.new("/app/custom_dir/templates")` would return the content defined in `/app/custom_dir/templates/content.html`. + +Let's say we want to write a `DatabaseTemplate` template loader: we first have to define a new class which inherits from `Marten::Template::Loader::Base`. This new class needs to define a `#get_template_source` method which takes a `template_name` string argument and also returns a string. + +For simplicity we assume that there already exists an `HtmlTemplate` model, featuring a `name` field and `content` field: + +```crystal +class DatabaseTemplate < Marten::Template::Loader::Base + def get_template_source(template_name) : String + begin + return HtmlTemplate.get!(name: template_name).content! + rescue e : Marten::DB::Errors::RecordNotFound + raise Marten::Template::Errors::TemplateNotFound.new("Template #{template_name} could not be found ; #{e.message}", e) + end + end +end +``` diff --git a/docs/versioned_docs/version-0.2/templates/how-to/create-custom-tags.md b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-tags.md similarity index 94% rename from docs/versioned_docs/version-0.2/templates/how-to/create-custom-tags.md rename to docs/versioned_docs/version-0.5/templates/how-to/create-custom-tags.md index 04e5d55df..23cf43d67 100644 --- a/docs/versioned_docs/version-0.2/templates/how-to/create-custom-tags.md +++ b/docs/versioned_docs/version-0.5/templates/how-to/create-custom-tags.md @@ -8,7 +8,7 @@ Marten has built-in support for common [template tags](../reference/tags.md), bu ## Defining a template tag -Template tags are subclasses of the [`Marten::Template::Tag::Base`](pathname:///api/0.2/Marten/Template/Tag/Base.html) abstract class. When writing custom template tags, you will usually want to define two methods in your tag classes: the `#initialize` and the `#render` methods. These two methods are called at different moments in a template's lifecycle: +Template tags are subclasses of the [`Marten::Template::Tag::Base`](pathname:///api/0.5/Marten/Template/Tag/Base.html) abstract class. When writing custom template tags, you will usually want to define two methods in your tag classes: the `#initialize` and the `#render` methods. These two methods are called at different moments in a template's lifecycle: * the `#initialize` method is used to initialize a template tag object and it is called at **parsing time**: this means that it is the responsibility of this method to ensure that the content of the template tag is valid from a parsing standpoint * the `#render` method is called at **rendering time** to apply the tag's logic: this means that the method is only called for valid template tag statements that were parsed without errors @@ -67,10 +67,10 @@ class LocalTimeTag < Marten::Template::Tag::Base end ``` -As you can see template tags are initialized from a parser (instance of [Marten::Template::Parser](pathname:///api/0.2/Marten/Template/Parser.html)) and the raw "source" of the template tag (that is the content between the `{%` and `%}` tag delimiters). The `#initialize` method is responsible for extracting any information that might be necessary to implement the template tag's logic. In the case of the `local_time` template tag, we must take care of a few things: +As you can see template tags are initialized from a parser (instance of [Marten::Template::Parser](pathname:///api/0.5/Marten/Template/Parser.html)) and the raw "source" of the template tag (that is the content between the `{%` and `%}` tag delimiters). The `#initialize` method is responsible for extracting any information that might be necessary to implement the template tag's logic. In the case of the `local_time` template tag, we must take care of a few things: * ensure that we have a format specified as argument (and raise an invalid syntax error otherwise) -* initialize a filter expression (instance of [Marten::Template::FilterExpression](pathname:///api/0.2/Marten/Template/FilterExpression.html)) from the format argument: this is necessary because the argument can be a string literal or variable with filters applied to it +* initialize a filter expression (instance of [Marten::Template::FilterExpression](pathname:///api/0.5/Marten/Template/FilterExpression.html)) from the format argument: this is necessary because the argument can be a string literal or variable with filters applied to it * verify if the output of the template tag is assigned to a variable by looking for an `as` statement: if that's the case the name of the variable is persisted in a dedicated instance variable The `#render` method is called at rendering time: it takes the current context object as argument and must return a string. In the above example, this method "resolves" the time format expression that was identified at initialization time from the context (which is necessary if it was a variable) and generates the right time representation. If the tag wasn't specified with an `as` variable, then this value is simply returned, otherwise, it is persisted in the context and an empty string is returned. @@ -141,7 +141,7 @@ end As you can see, the implementation of this tag looks quite similar to the one highlighted in [Simple tags](#simple-tags). The only differences that are worth noting here are: 1. the argument of the template tag corresponds to the list of items that should be rendered -2. the `#render` method explicitly renders the template mentioned previously by using a context with the "list" object in it (the [`#stack`](pathname:///api/0.2/Marten/Template/Context.html#stack(%26)%3ANil-instance-method) method allows to create a new context where new values are stacked over the existing ones). The output of this rendering operation is either assigned to a variable or returned directly depending on whether the `as` statement was used +2. the `#render` method explicitly renders the template mentioned previously by using a context with the "list" object in it (the [`#stack`](pathname:///api/0.5/Marten/Template/Context.html#stack(%26)%3ANil-instance-method) method allows to create a new context where new values are stacked over the existing ones). The output of this rendering operation is either assigned to a variable or returned directly depending on whether the `as` statement was used ### Closable tags @@ -164,7 +164,7 @@ class SpacelessTag < Marten::Template::Base end ``` -In this example, the `#initialize` method explicitly calls the parser's [`#parse`](pathname:///api/0.2/Marten/Template/Parser.html#parse(up_to%3AArray(String)%3F%3Dnil)%3ANodeSet-instance-method) in order to parse the following "nodes" up to the expected closing tag (`endspaceless` in this case). If the specified closing tag is not encountered, the parser will automatically raise a syntax error. The obtained nodes are returned as a "node set" (instance of [`Marten::Template::NodeSet`](pathname:///api/0.2/Marten/Template/NodeSet.html)): this is a special object returned by the template parser that maps to multiple parsed nodes (those can be tags, variables, or plain text values) that can be rendered through a [`#render`](pathname:///api/0.2/Marten/Template/NodeSet.html#render(context%3AContext)-instance-method) method at rendering time. +In this example, the `#initialize` method explicitly calls the parser's [`#parse`](pathname:///api/0.5/Marten/Template/Parser.html#parse(up_to%3AArray(String)%3F%3Dnil)%3ANodeSet-instance-method) in order to parse the following "nodes" up to the expected closing tag (`endspaceless` in this case). If the specified closing tag is not encountered, the parser will automatically raise a syntax error. The obtained nodes are returned as a "node set" (instance of [`Marten::Template::NodeSet`](pathname:///api/0.5/Marten/Template/NodeSet.html)): this is a special object returned by the template parser that maps to multiple parsed nodes (those can be tags, variables, or plain text values) that can be rendered through a [`#render`](pathname:///api/0.5/Marten/Template/NodeSet.html#render(context%3AContext)-instance-method) method at rendering time. The `#render` method of the above tag is relatively simple: it simply "renders" the node set corresponding to the template nodes that were extracted between the `{% spaceless %}...{% endspaceless %}` tags and then removes any whitespaces between the HTML tags in the output. @@ -172,7 +172,7 @@ The `#render` method of the above tag is relatively simple: it simply "renders" In order to be able to use custom template tags, you must register them to Marten's global template tags registry. -To do so, you will have to call the [`Marten::Template::Tag#register`](pathname:///api/0.2/Marten/Template/Tag.html#register(tag_name%3AString|Symbol%2Ctag_klass%3ABase.class)-class-method) method with the name of the tag you wish to use in templates, and the template tag class. +To do so, you will have to call the [`Marten::Template::Tag#register`](pathname:///api/0.5/Marten/Template/Tag.html#register(tag_name%3AString|Symbol%2Ctag_klass%3ABase.class)-class-method) method with the name of the tag you wish to use in templates, and the template tag class. For example: diff --git a/docs/versioned_docs/version-0.2/templates/introduction.md b/docs/versioned_docs/version-0.5/templates/introduction.md similarity index 73% rename from docs/versioned_docs/version-0.2/templates/introduction.md rename to docs/versioned_docs/version-0.5/templates/introduction.md index 627fa0291..eee1fd88b 100644 --- a/docs/versioned_docs/version-0.2/templates/introduction.md +++ b/docs/versioned_docs/version-0.5/templates/introduction.md @@ -70,7 +70,7 @@ For example, the following snippet will apply the [`default`](./reference/filter Hello, {{ name|default:"Stranger" }}! ``` -It should be noted that the fact that an argument is supported or not, and mandatory or not, varies based on the considered filter. In all cases, filters can support up to **one** argument only. +It should be noted that the fact that an argument is supported or not, and mandatory or not, varies based on the considered filter. In all cases, filters can support up to **one** argument only. Moreover, a filter argument can either correspond to a regular [variable](#variables) or a [literal](#literal-values). Please head over to the [filters reference](./reference/filters.md) to see a list of all the available filters. Implementing custom filters is also a possibility that is documented in [Create custom filters](./how-to/create-custom-filters.md). @@ -88,11 +88,11 @@ As mentioned above, some tags allow to perform control flows and require a "clos ```html {% for article in articles %} - {{ article.title }} is {% if not article.published? %}not {% endif %}published + {{ article.title }} is {% if !article.published? %}not {% endif %}published {% endfor %} ``` -Some tags also require arguments. For example, the [`url`](./reference/tags.md#url) template tag requires at least the name of the route for which the URL resolution should be performed: +Some tags also require arguments. Similarly to filters, these arguments can either correspond to regular [variables](#variables) or [literals](#literal-values). For example, the [`url`](./reference/tags.md#url) template tag requires at least the name of the route for which the URL resolution should be performed: ```html {% url "my_route" %} @@ -108,6 +108,20 @@ Comments can be inserted in any templates and must be surrounded by **`{#`** and {# This will not be evaluated #} ``` +### Literal values + +The Marten templating language supports making use of literal values as part of [variables](#variables), [filter](#filters) arguments, or [tag](#tags) arguments. These literals are essentially a representation of the corresponding objects in Crystal. Each supported type of literal is listed below: + +| Type | Example | +| ----------- | ----------- | +| Nil | `{{ nil }}` | +| True | `{{ true }}` | +| False | `{{ false }}` | +| Integer | `{{ 42 }}` | +| Float | `{{ 42.45 }}` | +| Single-quoted string | `{{ 'Hello World' }}` | +| Double-quoted string | `{{ "Hello World" }}` | + ## Template inheritance Templates can inherit from each other: this allows you to easily define a "base" template containing the layout of your application so that you can reuse it in order to build other templates, which helps in keeping your codebase DRY. @@ -178,28 +192,68 @@ For example, with the following snippet the output of the `title` block would be It's important to remember that the `super` template tag can only be used within `block` tags. +## Template inclusion + +Templates can "include" other templates easily through the use of the [`include`](./reference/tags.md#include) template tag. Such templates are usually referred to as "partials": these are template snippets that can be easily "included" into other templates to avoid duplications of code. + +Included templates are rendered using the context of the including template. This means that all the variables that are provided to the including template can also be used as part of the included template. In addition to that, other specific variables can also be defined when including a specific template. + +For example, let's assume that a project defines the following partial template: + +```html name="src/templates/partials/button.html + +``` + +This partial could be included as follows in the following template: + +```html +{% include "partials/button.html" with type="primary" %} +{% include "partials/button.html" with type="primary", text="Custom text" %} +``` + +Note the use of the `with` keyword to specify comma-separated variables that should be used to populate the included template's context. + +Obviously, it is also possible to include partials that don't require any variables. In that case, the use of the `with` keyword is not necessary: + +```html +{% include "partials/other_snippet.html" %} +``` + ## Template loading Templates can be loaded from specific locations within your codebase and from application folders. This is controlled by two main settings: * [`templates.app_dirs`](../development/reference/settings.md#app_dirs-1) is a boolean that indicates whether or not it should be possible to load templates that are provided by [installed applications](../development/reference/settings.md#installed_apps). Indeed, applications can define a `templates` folder at their root, and these templates will be discoverable by Marten if this setting is set to `true` -* [`templates.dirs`](../development/reference/settings.md#dirs1) is an array of additional directories where templates should be looked for +* [`templates.dirs`](../development/reference/settings.md#dirs-1) is an array of additional directories where templates should be looked for Application templates are always enabled by default (`templates.app_dirs = true`) for new Marten projects. -It is possible to programmatically load a template by name. To do so, you can use the [`#get_template`](pathname:///api/0.2/Marten/Template/Engine.html#get_template(template_name%3AString)%3ATemplate-instance-method) method that is provided by the Marten templates engine: +It is possible to programmatically load a template by name. To do so, you can use the [`#get_template`](pathname:///api/0.5/Marten/Template/Engine.html#get_template(template_name%3AString)%3ATemplate-instance-method) method that is provided by the Marten templates engine: ```crystal Marten.templates.get_template("foo/bar.html") ``` -This will return a compiled [`Template`](pathname:///api/0.2/Marten/Template/Template.html) object that you can then render by using a specific context. +This will return a compiled [`Template`](pathname:///api/0.5/Marten/Template/Template.html) object that you can then render by using a specific context. + +:::tip Customizing template loaders +The [`templates.loaders`](../development/reference/settings.md#loaders) setting provides fine-grained control over how Marten discovers and loads templates. This setting expects an array of template loader classes, which must inherit from `Marten::Template::Loader::Base`. This setting will overwrite the default template loaders +configured by Marten. This gives you the option to configure [custom loaders](./how-to/create-custom-loaders.md) to load templates from different sources, such as databases or in-memory data structures. + +Example: Configuring a File System Loader + +```crystal +config.templates.loaders = [Marten::Template::Loader::FileSystem.new("/path/to/templates")] of Marten::Template::Loader::Base +``` +::: ## Rendering a template You won't usually need to interact with the "low-level" API of the Marten template engine in order to render templates: most of the time you will render templates as part of [handlers](../handlers-and-http.mdx), which means that you will likely end up using the [`#render`](../handlers-and-http/introduction.md#render) shortcut or [generic handlers](../handlers-and-http/generic-handlers.md) that automatically render templates for you. -That being said, it is also possible to render any [`Template`](pathname:///api/0.2/Marten/Template/Template.html) object that you loaded by leveraging the [`#render`](pathname:///api/0.2/Marten/Template/Template.html#render(context%3AHash|NamedTuple)%3AString-instance-method) method. This method can be used either with a Marten context object, a hash, or a named tuple: +That being said, it is also possible to render any [`Template`](pathname:///api/0.5/Marten/Template/Template.html) object that you loaded by leveraging the [`#render`](pathname:///api/0.5/Marten/Template/Template.html#render(context%3AHash|NamedTuple)%3AString-instance-method) method. This method can be used either with a Marten context object, a hash, or a named tuple: ```crystal template = Marten.templates.get_template("foo/bar.html") @@ -208,12 +262,42 @@ template.render({"foo" => "bar"}) template.render({ foo: "bar" }) ``` +## Using enums in contexts + +[Enum](https://crystal-lang.org/api/Enum.html) classes cannot be part of union types in Crystal. Because of this limitation, enum values are replaced by special objects in Marten templates. These objects always resolve to the integer value of the corresponding enum value. + +For example, let's consider the following enum: + +```crystal +enum Color + Red + Green + Blue +end +``` + +If a `color` variable is set to `Color::Red`, then the following template will output `0`: + +```html +{{ color }} +``` + +It's important to note that enum values within Marten templates can be compared with each other, yielding the same results as comparing enum values in Crystal. + +Additionally, `?` helper properties can be invoked on enum values within templates, simplifying the process of determining the type of the enum value being considered. For instance: + +```html +{% if color.red? %} + This is the red color. +{% endif %} +``` + ## Using custom objects in contexts -Most objects that are provided by Marten (such as Model records, query sets, schemas, etc) can automatically be used as part of templates. If your project involves other custom classes, and if you would like to interact with such objects in your templates, then you will need to explicitly ensure that they include the [`Marten::Template::Object`](pathname:///api/0.2/Marten/Template/Object.html) module. +Most objects that are provided by Marten (such as Model records, query sets, schemas, etc) can automatically be used as part of templates. If your project involves other custom classes, and if you would like to interact with such objects in your templates, then you will need to explicitly ensure that they include the [`Marten::Template::Object`](pathname:///api/0.5/Marten/Template/Object.html) module. :::note Why? -Crystal being a statically typed language, the Marten engine needs to know which types of objects it is dealing with in advance in order to know (i) what can go into template contexts and (ii) how to "resolve" object attributes when templates are rendered. It is not possible to simply expect any `Object` object, hence why we need to make use of a shared [`Marten::Template::Object`](pathname:///api/0.2/Marten/Template/Object.html) module to account for all the classes whose objects should be usable as part of template contexts. +Crystal being a statically typed language, the Marten engine needs to know which types of objects it is dealing with in advance in order to know (i) what can go into template contexts and (ii) how to "resolve" object attributes when templates are rendered. It is not possible to simply expect any `Object` object, hence why we need to make use of a shared [`Marten::Template::Object`](pathname:///api/0.5/Marten/Template/Object.html) module to account for all the classes whose objects should be usable as part of template contexts. ::: Let's take the example of a `Point` class that provides access to an x-coordinate and a y-coordinate: @@ -240,7 +324,7 @@ If you try to render such a template while passing a `Point` object into the tem Unable to initialize template values from Point objects ``` -To remediate this, you will have to include the [`Marten::Template::Object`](pathname:///api/0.2/Marten/Template/Object.html) module in the `Point` class and define a `#resolve_template_attribute` method as follows: +To remediate this, you will have to include the [`Marten::Template::Object`](pathname:///api/0.5/Marten/Template/Object.html) module in the `Point` class and define a `#resolve_template_attribute` method as follows: ```crystal class Point @@ -263,9 +347,9 @@ class Point end ``` -Each class including the [`Marten::Template::Object`](pathname:///api/0.2/Marten/Template/Object.html) module must also implement a `#resolve_template_attribute` method in order to allow resolutions of object attributes when templates are rendered (for example `{{ point.x }}`). That being said, there are a few shortcuts that can be used in order to avoid writing such methods. +Each class including the [`Marten::Template::Object`](pathname:///api/0.5/Marten/Template/Object.html) module must also implement a `#resolve_template_attribute` method in order to allow resolutions of object attributes when templates are rendered (for example `{{ point.x }}`). That being said, there are a few shortcuts that can be used in order to avoid writing such methods. -The first one is to use the [`#template_attributes`](pathname:///api/0.2/Marten/Template/Object.html#template_attributes(*names)-macro) macro in order to easily define the names of the methods that should be made available to the template runtime. For example, such macro could be used like this with our `Point` class: +The first one is to use the [`#template_attributes`](pathname:///api/0.5/Marten/Template/Object.html#template_attributes(*names)-macro) macro in order to easily define the names of the methods that should be made available to the template runtime. For example, such macro could be used like this with our `Point` class: ```crystal class Point @@ -281,7 +365,7 @@ class Point end ``` -Another possibility is to include the [`Marten::Template::Object::Auto`](pathname:///api/0.2/Marten/Template/Object/Auto.html) module instead of the [`Marten::Template::Object`](pathname:///api/0.2/Marten/Template/Object.html) one in your class. This module will automatically ensure that every "attribute-like" public method that is defined in the including class can also be accessed in templates when performing variable lookups. +Another possibility is to include the [`Marten::Template::Object::Auto`](pathname:///api/0.5/Marten/Template/Object/Auto.html) module instead of the [`Marten::Template::Object`](pathname:///api/0.5/Marten/Template/Object.html) one in your class. This module will automatically ensure that every "attribute-like" public method that is defined in the including class can also be accessed in templates when performing variable lookups. ```crystal class Point @@ -295,15 +379,15 @@ class Point end ``` -Note that **all** "attribute-like" public methods will be made available to the template runtime when using the [`Marten::Template::Object::Auto`](pathname:///api/0.2/Marten/Template/Object/Auto.html) module. This may be a good enough behavior, but if you want to have more control over what can be accessed in templates or not, you will likely end up using [`Marten::Template::Object`](pathname:///api/0.2/Marten/Template/Object.html) and the [`#template_attributes`](pathname:///api/0.2/Marten/Template/Object.html#template_attributes(*names)-macro) macro instead. +Note that **all** "attribute-like" public methods will be made available to the template runtime when using the [`Marten::Template::Object::Auto`](pathname:///api/0.5/Marten/Template/Object/Auto.html) module. This may be a good enough behavior, but if you want to have more control over what can be accessed in templates or not, you will likely end up using [`Marten::Template::Object`](pathname:///api/0.5/Marten/Template/Object.html) and the [`#template_attributes`](pathname:///api/0.5/Marten/Template/Object.html#template_attributes(*names)-macro) macro instead. ## Using context producers Context producers are helpers that ensure that common variables are automatically inserted in the template context whenever a template is rendered. They are applied every time a new template context is generated. -For example, they can be used to insert the current HTTP request object in every template context being rendered in the context of a handler and HTTP request. This makes sense considering that the HTTP request object is a common object that is likely to be used by multiple templates in your project: that way there is no need to explicitly "insert" it in the context every time you render a template. This specific capability is provided by the [`Marten::Template::ContextProducer::Request`](pathname:///api/0.2/Marten/Template/ContextProducer/Request.html) context producer, which inserts a `request` object into every template context. +For example, they can be used to insert the current HTTP request object in every template context being rendered in the context of a handler and HTTP request. This makes sense considering that the HTTP request object is a common object that is likely to be used by multiple templates in your project: that way there is no need to explicitly "insert" it in the context every time you render a template. This specific capability is provided by the [`Marten::Template::ContextProducer::Request`](pathname:///api/0.5/Marten/Template/ContextProducer/Request.html) context producer, which inserts a `request` object into every template context. -Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#contextproducers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: +Template context producers can be configured through the use of the [`templates.context_producers`](../development/reference/settings.md#context_producers) setting. When generating a new project by using the `marten new` command, the following context producers will be automatically configured: ```crystal config.templates.context_producers = [ @@ -351,3 +435,9 @@ When rendered with `John` as the content of the `name` variable, the abov Hello, <b>John</b>! Hello, John! ``` + +## Strict variables + +By default, when a template variable is unknown or undefined, Marten treats it as a `nil` value. Consequently, nothing will be displayed for such variables, and they will be evaluated as falsey in if conditions. + +However, it is possible to modify this behavior by enabling the [`templates.strict_variables`](../development/reference/settings.md#strict_variables) setting. When this setting is set to `true`, unknown variables encountered in templates will raise [`Marten::Template::Errors::UnknownVariable`](pathname:///api/0.5/Marten/Template/Errors/UnknownVariable.html) exceptions. diff --git a/docs/versioned_docs/version-0.2/templates/reference/context-producers.md b/docs/versioned_docs/version-0.5/templates/reference/context-producers.md similarity index 87% rename from docs/versioned_docs/version-0.2/templates/reference/context-producers.md rename to docs/versioned_docs/version-0.5/templates/reference/context-producers.md index 3b7ffdcf1..b00e76004 100644 --- a/docs/versioned_docs/version-0.2/templates/reference/context-producers.md +++ b/docs/versioned_docs/version-0.5/templates/reference/context-producers.md @@ -7,19 +7,19 @@ This page provides a reference for all the available context producers that can ## Debug context producer -**Class:** [`Marten::Template::ContextProducer::Debug`](pathname:///api/0.2/Marten/Template/ContextProducer/Debug.html) +**Class:** [`Marten::Template::ContextProducer::Debug`](pathname:///api/0.5/Marten/Template/ContextProducer/Debug.html) The Debug context producer contributes a `debug` variable to the context: the associated value is `true` or `false` depending on whether [debug mode](../../development/reference/settings.md#debug) is enabled for the project or not. ## Flash context producer -**Class:** [`Marten::Template::ContextProducer::Flash`](pathname:///api/0.2/Marten/Template/ContextProducer/Flash.html) +**Class:** [`Marten::Template::ContextProducer::Flash`](pathname:///api/0.5/Marten/Template/ContextProducer/Flash.html) The Flash context producer contributes a `flash` variable to the context: this variable corresponds to the [flash store](../../handlers-and-http/introduction.md#using-the-flash-store) that is associated with the current HTTP request. If the template context is not initialized with an HTTP request object, then no variables are inserted. ## I18n context producer -**Class:** [`Marten::Template::ContextProducer::I18n`](pathname:///api/0.2/Marten/Template/ContextProducer/I18n.html) +**Class:** [`Marten::Template::ContextProducer::I18n`](pathname:///api/0.5/Marten/Template/ContextProducer/I18n.html) The I18n context producer contributes I18n-related variables to the context: @@ -28,7 +28,7 @@ The I18n context producer contributes I18n-related variables to the context: ## Request context producer -**Class:** [`Marten::Template::ContextProducer::Request`](pathname:///api/0.2/Marten/Template/ContextProducer/Request.html) +**Class:** [`Marten::Template::ContextProducer::Request`](pathname:///api/0.5/Marten/Template/ContextProducer/Request.html) The Request context producer contributes a `request` variable to the context: this variable corresponds to the current HTTP request object. If the template context is not initialized with an HTTP request object, then no variables are inserted. diff --git a/docs/versioned_docs/version-0.2/templates/reference/filters.md b/docs/versioned_docs/version-0.5/templates/reference/filters.md similarity index 56% rename from docs/versioned_docs/version-0.2/templates/reference/filters.md rename to docs/versioned_docs/version-0.5/templates/reference/filters.md index 5aff202af..d95474751 100644 --- a/docs/versioned_docs/version-0.2/templates/reference/filters.md +++ b/docs/versioned_docs/version-0.5/templates/reference/filters.md @@ -19,7 +19,7 @@ If `value` is "marten", the output will be "Marten". ## `default` -The `default` filter allows to fallback to a specific value if the left side of the filter expression is not truthy. A filter argument is mandatory. It should be noted that empty strings are considered truthy and will be returned by this filter. +The `default` filter allows to fallback to a specific value if the left side of the filter expression is empty or not truthy. A filter argument is mandatory. It should be noted that empty strings are considered truthy and will be returned by this filter. For example: @@ -41,6 +41,30 @@ For example: If `value` is "Hello", then the output will be "hello". +## `escape` + +The `escape` filter replaces special characters (namely `&`, `<`, `>`, `"` and `'`) in the template variable with their corresponding HTML entities. + +For example: + +```html +{{ value|escape }} +``` + +If `value` is `Let's do it`, then the output will be `<b>Let's do it</b>`. + +## `join` + +The `join` filter converts an array of elements into a string separated by `arg`. + +For example: + +```html +{{ value|join: arg }} +``` + +If `value` is `["Bananas","Apples","Oranges"]` and `arg` is `, `, then the output will be "Bananas, Apples, Oranges". + ## `linebreaks` The `linebreaks` filter allows to convert a string replacing all newlines with HTML line breaks (`
`). @@ -75,7 +99,27 @@ For example: {{ value|size }} ``` -If `value` is `hello`, then the output will be 5. +## `split` + +The `split` filter converts a string into an array of elements separated by `arg`. + +For example: + +```html +{{ value|split: arg }} +``` + +If `value` is `Bananas,Apples,Oranges` and `arg` is `,`, then the output will be ["Bananas","Apples","Oranges"]. + +## `time` + +The `time` filter allows outputting the string representation of a time variable. It requires the specification of a filter argument, which is the format string used to format the time (whose available directives are part of [`Time::Format`](https://crystal-lang.org/api/Time/Format.html)). + +```html +{{ value | time: "%Y-%m-%d" }} +``` + +In the above example, the output will be a date string such as `2023-09-25`. ## `upcase` diff --git a/docs/versioned_docs/version-0.5/templates/reference/loaders.md b/docs/versioned_docs/version-0.5/templates/reference/loaders.md new file mode 100644 index 000000000..6590cea0e --- /dev/null +++ b/docs/versioned_docs/version-0.5/templates/reference/loaders.md @@ -0,0 +1,43 @@ +--- +title: Template loaders +description: Template loaders reference. +--- + +This page provides a reference for all the available template loaders that can be used to customize template retrieval in Marten. + +## FileSystem Loader + +**Class**: [`Marten::Template::Loader::FileSystem`](pathname:///api/0.5/Marten/Template/Loader/FileSystem.html) + +Loads templates directly from the file system. + +Initialization example: + +```crystal +loader = Marten::Template::Loader::FileSystem.new("/path/to/templates") +``` + +## AppDirs Loader + +**Class**: [`Marten::Template::Loader::AppDirs`](pathname:///api/0.5/Marten/Template/Loader/AppDirs.html) + +Coordinates template loading from application directories. Relies on instances of FileSystem. + +Initialization example: + +```crystal +loader = Marten::Template::Loader::AppDirs.new +``` + +## Cached Loader + +**Class**: [`Marten::Template::Loader::Cached`](pathname:///api/0.5/Marten/Template/Loader/Cached.html) + +Provides a caching layer for compiled templates. Can wrap other loaders to optimize retrieval. + +Initialization example: + +```crystal +file_loader = Marten::Template::Loader::FileSystem.new("/path/to/templates") +loader = Marten::Template::Loader::Cached.new([file_loader] of Marten::Template::Loader::Base) +``` diff --git a/docs/versioned_docs/version-0.2/templates/reference/tags.md b/docs/versioned_docs/version-0.5/templates/reference/tags.md similarity index 50% rename from docs/versioned_docs/version-0.2/templates/reference/tags.md rename to docs/versioned_docs/version-0.5/templates/reference/tags.md index aa36e2ac2..aaf0a76bc 100644 --- a/docs/versioned_docs/version-0.2/templates/reference/tags.md +++ b/docs/versioned_docs/version-0.5/templates/reference/tags.md @@ -29,13 +29,100 @@ For example: {% assign my_var = "Hello World!" %} ``` +By default, variables assigned using this template tag will overwrite any existing variables with the same name in the template context. To prevent overwriting existing variables, you can append `unless assigned` after the assignment to ensure that new variables are only assigned if there isn't already one with the same name present. + +For example: + +```html +{% assign my_var = "Hello World!" unless defined %} +``` + ## `block` The `block` template tag allows to define that some specific portions of a template can be overridden by child templates. This tag is only useful when used in conjunction with the [`extend`](#extend) tag. See [Template inheritance](../introduction.md#template-inheritance) to learn more about this capability. +## `cache` + +The `cache` template tag allows to cache the content of a template fragment (enclosed within the `{% cache %}...{% endcache %}` tags) for a specific duration. This caching operation is done by leveraging the configured [cache store](../../caching/introduction.md#configuration-and-cache-stores). + +At least a cache key and and a cache expiry (expressed in seconds) must be specified when using this tag: + +```html +{% cache "mykey" 3600 %} + Cached content! +{% endcache %} +``` + +It should be noted that the `cache` template tag also supports specifying additional "vary on" arguments that allow to invalidate the cache based on the value of other template variables: + +```html +{% cache "mykey" 3600 current_locale user.id %} + Cached content! +{% endcache %} +``` + +## `capture` + +The `capture` template tag allows to define that the output of a block of code should be stored in a new variable. + +For example: + +```html +{% capture my_var %} + Hello World, {{ name }}! +{% endcapture %} +``` + +Assuming the variable `name` is assigned the value "John Doe" upon rendering this snippet, the variable `my_var` will hold the string "Hello World, John Doe!". + +By default, variables assigned using this template tag will overwrite any existing variables with the same name in the template context. To prevent overwriting existing variables, you can append `unless assigned` after the variable name to ensure that new variables are only assigned if there isn't already one with the same name present. + +For example: + +```html +{% capture my_var unless defined %} + Hello World, {{ name }}! +{% endcapture %} +``` + +## `csrf_input` + +The `csrf_input` template tag allows generating a hidden HTML input containing the CSRF token (computed for the request at hand). This tag can only be used in templates that are rendered as part of a handler (for example by leveraging [`#render`](../../handlers-and-http/introduction.md#render) or one of the [generic handlers](../../handlers-and-http/generic-handlers.md) involving rendered templates). + +This can be used to ensure the CSRF token gets inserted into a form so that it gets sent to the handler processing the form data for example. Indeed, handlers will automatically perform a CSRF check in order to protect unsafe requests (ie. requests whose methods are not `GET`, `HEAD`, `OPTIONS`, or `TRACE`): + +```html + + {% csrf_input %} + + + +``` + +The above template will output the following HTML: + +```html +
+ // highlight-next-line + + + +
+``` + +Where `` is the actual CSRF token. + +See [Cross-Site Request Forgery protection](../../security/csrf.md) to learn more about this. + +Optionally, the output of the `csrf_input` template tag can be assigned to a specific variable using the `as` keyword: + +```html +{% csrf_input as my_var %} +``` + ## `csrf_token` -The `csrf_token` template tag allows to compute and insert the value of the CSRF token into a template. This tag can only be used for templates that are rendered as part of a handler (for example by leveraging [`#render`](../../handlers-and-http/introduction.md#render) or one of the [generic handlers](../../handlers-and-http/generic-handlers.md) involving rendered templates). +The `csrf_token` template tag allows to compute and insert the value of the CSRF token into a template. This tag can only be used in templates that are rendered as part of a handler (for example by leveraging [`#render`](../../handlers-and-http/introduction.md#render) or one of the [generic handlers](../../handlers-and-http/generic-handlers.md) involving rendered templates). This can be used to insert the CSRF token into a hidden form input so that it gets sent to the handler processing the form data for example. Indeed, handlers will automatically perform a CSRF check in order to protect unsafe requests (ie. requests whose methods are not `GET`, `HEAD`, `OPTIONS`, or `TRACE`): @@ -49,6 +136,24 @@ This can be used to insert the CSRF token into a hidden form input so that it ge See [Cross-Site Request Forgery protection](../../security/csrf.md) to learn more about this. +Optionally, the output of the `csrf_token` template tag can be assigned to a specific variable using the `as` keyword: + +```html +{% csrf_token as my_var %} +``` + +## `escape` + +The `escape` tag is used to enable or disable [auto-escaping](../introduction.md#auto-escaping) for a block of code. It takes one argument, either `on` or `off`, to enable or disable auto-escaping, respectively. + +For example: + +```html +{% escape off %} +
{{ article.html_body }}
+{% endescape %} +``` + ## `extend` The `extend` template tag allows to define that a template inherits from a specific base template. This tag must be used with one mandatory argument, which can be either a string literal or a variable that will be resolved at runtime. This mechanism is useful only if the base template defines [blocks](#block) that are overridden or extended by the child template. See [Template inheritance](../introduction.md#template-inheritance) to learn more about this capability. @@ -85,24 +190,47 @@ Finally, loops give access to a special `loop` variable _inside_ the loop in ord | `loop.revindex0` | The index of the current iteration counting from the end of the loop (0-indexed) | | `loop.first?` | A boolean indicating if this is the first iteration of the loop | | `loop.last?` | A boolean indicating if this is the last iteration of the loop | +| `loop.even?` | A boolean indicating if the index of the current iteration (0-indexed) is even | +| `loop.odd?` | A boolean indicating if the index of the current iteration (0-indexed) is odd | | `loop.parent` | The parent's `loop` variable (only for nested for loops) | ## `if` -The `if` template tags allows to define conditions allowing to control which blocks should be executed. An `if` tag must always start with an `if` condition, followed by any number of intermediate `elsif` conditions and an optional (and final) `else` block. It also requires a closing `endif` tag. +The `if` template tag makes it possible to define conditions allowing to control which blocks should be executed. An `if` tag must always start with an `if` condition, followed by any number of intermediate `elsif` conditions and an optional (and final) `else` block. It also requires a closing `endif` tag. For example: ```html {% if my_var == 0 %} Zero! -{% elsif my_var == 1 %} +{% elsif my_var == 1 && other_var == "foobar" %} One! +{% elsif !additional_var %} + Something else! {% else %} Other! {% endif %} ``` +The following equality and comparison operators can be used as part of `if` conditions: + +| Operator | Description | +| -------- | ----------- | +| `==` | Equals | +| `!=` | Not equals | +| `>` | Greater than | +| `>=` | Greater than or equals | +| `<` | Less than | +| `<=` | Greater than or equals | + +Additionally, the following logical operators can be used as part of `if` conditions: + +| Operator | Description | +| -------- | ----------- | +| `&&` | Logical AND | +| `\|\|` | Logical OR | +| `!` or `not` | Logical negation | + ## `include` The `include` template tag allows to include and render another template using the current context. This tag must be used with one mandatory argument: the name of the template to include, which can be either a string literal or a variable that will be resolved at runtime. @@ -127,12 +255,28 @@ How are you {{ name }}? If `name` is "John", then the output will be "Hello, John! How are you John?". -Finally, it should be noted that additional variables that are specific to the included template only can be specified using the `with` keyword: +It should be noted that additional variables that are specific to the included template only can be specified using the `with` keyword: ```html {% include "path/to/my_snippet.html" with new_var="hello" %} ``` +Multiple variables can also be specified if necessary. In that case, variable assignments must be separated by commas. For example: + +```html +{% include "path/to/my_snippet.html" with var1="foo", var2="bar" %} +``` + +Additionally, it is important to note that the accessibility of outer context variables for included templates depends on the value of the [`templates.isolated_inclusions`](../../development/reference/settings.md#isolated_inclusions) setting. By default, this setting is set to `false`, which means that included templates have access to the outer context variables. However, it is important to note that this behavior can be modified for each inclusion, regardless of the value of the [`templates.isolated_inclusions`](../../development/reference/settings.md#isolated_inclusions) setting. This can be achieved by appending the `isolated` modifier to specify that the included template must not access the outer context, or using the `contextual` modifier to indicate that it should have access. For example: + +```html + +{% include "path/to/my_snippet.html" with new_var="hello" isolated %} + + +{% include "path/to/my_snippet.html" with new_var="hello" contextual %} +``` + :::caution Templates that are included using the `include` template are parsed and rendered _when_ the including template is rendered as well. Included templates are not parsed when the including template is parsed itself. This means that the including template and the included template are always rendered _separately_. ::: @@ -154,6 +298,23 @@ Optionally, the output of this tag can be assigned to a specific variable using {% local_time "%Y" as current_year %} ``` +## `method_input` + +The `method_input` template tag creates a hidden form input tag. This input tag has the name `_method` and gets the value assigned provided by the first tag argument + +For example: + +```html +
+ {% method_input "DELETE" %} +
+ +``` + ## `spaceless` The `spaceless` template tag allows to remove whitespaces, tabs, and new lines between HTML tags. Whitespaces inside tags are left untouched. It should be noted that the `spaceless` template tag requires a closing `endspaceless` tag. @@ -205,6 +366,22 @@ Alias for [`translate`](#translate). Alias for [`translate`](#translate). +## `unless` + +The `unless` template tag makes it possible to define conditions allowing to control which blocks should be executed. An `unless` tag must always start with an `unless` condition, followed by an optional (and final) `else` block. It also requires a closing `endunless` tag. + +For example: + +```html +{% unless my_var == 0 %} + Other value! +{% else %} + Zero! +{% endunless %} +``` + +The `unless` template tag supports the same equality, comparison, and logical operators as the ones supported by the [`if`](#if) template tag. + ## `url` The `url` template tag allows to perform [URL lookups](../../handlers-and-http/routing.md#reverse-url-resolutions). It must take at least one argument (the name of the targeted handler) followed by optional keyword arguments (if the route requires parameters). @@ -237,3 +414,17 @@ For example: ``` Would output `This should not be {{ processed }}.`. + +## `with` + +The `with` template tag assigns one or more variables inside a block. After the end of the block has been reached the block variables are no longer available. + +For example: + +``` +{% with x = 'Hello World', y = 1 %} + {{ x }} {{ y }}! +{% endwith %} +``` + +Would output `Hello World 1!`. diff --git a/docs/versioned_docs/version-0.2/the-marten-project.mdx b/docs/versioned_docs/version-0.5/the-marten-project.mdx similarity index 79% rename from docs/versioned_docs/version-0.2/the-marten-project.mdx rename to docs/versioned_docs/version-0.5/the-marten-project.mdx index 0b93daa5b..383cf00ee 100644 --- a/docs/versioned_docs/version-0.2/the-marten-project.mdx +++ b/docs/versioned_docs/version-0.5/the-marten-project.mdx @@ -12,6 +12,9 @@ This section provides general information about the Marten project itself and de
+
+ +
diff --git a/docs/versioned_docs/version-0.2/the-marten-project/acknowledgments.md b/docs/versioned_docs/version-0.5/the-marten-project/acknowledgments.md similarity index 95% rename from docs/versioned_docs/version-0.2/the-marten-project/acknowledgments.md rename to docs/versioned_docs/version-0.5/the-marten-project/acknowledgments.md index 1df54953a..0a0640da7 100644 --- a/docs/versioned_docs/version-0.2/the-marten-project/acknowledgments.md +++ b/docs/versioned_docs/version-0.5/the-marten-project/acknowledgments.md @@ -27,7 +27,7 @@ The Marten web framework is also inspired by [Ruby on Rails](https://rubyonrails * The [generic validation DSL](../models-and-databases/validations.md) * Most [model callbacks](../models-and-databases/callbacks.md) -* The idea of [message encryptors](pathname:///api/0.2/Marten/Core/Encryptor.html) and [message signers](pathname:///api/0.2/Marten/Core/Signer.html) +* The idea of [message encryptors](pathname:///api/0.5/Marten/Core/Encryptor.html) and [message signers](pathname:///api/0.5/Marten/Core/Signer.html) ### But also... diff --git a/docs/versioned_docs/version-0.2/the-marten-project/contributing.md b/docs/versioned_docs/version-0.5/the-marten-project/contributing.md similarity index 81% rename from docs/versioned_docs/version-0.2/the-marten-project/contributing.md rename to docs/versioned_docs/version-0.5/the-marten-project/contributing.md index 3b4fc475b..1996f109c 100644 --- a/docs/versioned_docs/version-0.2/the-marten-project/contributing.md +++ b/docs/versioned_docs/version-0.5/the-marten-project/contributing.md @@ -48,6 +48,35 @@ make This will install a bunch of Crystal shards and some Node.js dependencies (which are required to work on the [Docusaurus](https://docusaurus.io/)-powered documentation). +### Setting up a test project with Marten + +This section dives into creating and configuring a test project using the Marten development environment. This allows you to experiment and develop features in isolation, ensuring everything works smoothly before committing changes to the main codebase. + +#### Creating a new test project + +Start by creating a new test project using the Marten CLI tool: + +```bash +./path/to/development/marten new project test-project [options] +``` + +Replace `test-project` with your desired project name. After installation change to the project directory. + +#### Configuring the Test Environment + +To use the development marten project instead of the one provided by `shard.yml`, you need to create a `shard.override.yml`. + +```yaml +name: test-project +version: 0.1.0 + +dependencies: + marten: + path: /path/to/development/marten +``` + +Run `shards install`. Now, your test project is configured to use your local Marten development environment. + ### Coding style Overall, Marten tries to comply with Crystal's [style guide](https://crystal-lang.org/reference/conventions/coding_style.html) and you should ensure that your changes comply with it as well if you are contributing code to the project. @@ -80,6 +109,11 @@ By default, specs will be executed using an in-memory SQLite database. If you wi ```json title=.spec.env.json { + "MARIADB_DEFAULT_DB_NAME": "example", + "MARIADB_OTHER_DB_NAME": "other_example", + "MARIADB_DB_USER": "example", + "MARIADB_DB_PASSWORD": "", + "MARIADB_DB_HOST": "", "MYSQL_DEFAULT_DB_NAME": "example", "MYSQL_OTHER_DB_NAME": "other_example", "MYSQL_DB_USER": "example", @@ -93,11 +127,12 @@ By default, specs will be executed using an in-memory SQLite database. If you wi } ``` -As you can see, you have to specify two databases for each database backend (MySQL and PostgreSQL). This is mandatory because Marten's specs are also testing cases where multiple databases are configured and used simultaneously for the same project. +As you can see, you have to specify two databases for each database backend (MariaDB, MySQL, and PostgreSQL). This is mandatory because Marten's specs are also testing cases where multiple databases are configured and used simultaneously for the same project. Specs are always executed using a _single_ database backend. As mentioned previously this database backend is the SQLite one by default, but you can specify the one to use when running specs by setting the `MARTEN_SPEC_DB_CONNECTION` environment variable. For example: ```bash +MARTEN_SPEC_DB_CONNECTION=mariadb make tests # Will run specs using the MariaDB DB backend MARTEN_SPEC_DB_CONNECTION=mysql make tests # Will run specs using the MySQL DB backend MARTEN_SPEC_DB_CONNECTION=postgresql make tests # Will run specs using the PostgreSQL DB backend ``` diff --git a/docs/versioned_docs/version-0.5/the-marten-project/design-philosophies.md b/docs/versioned_docs/version-0.5/the-marten-project/design-philosophies.md new file mode 100644 index 000000000..1406f8c8b --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/design-philosophies.md @@ -0,0 +1,34 @@ +--- +title: Design philosophies +description: Learn about the design philosophies behind the Marten web framework. +--- + +This document goes over the fundamental design philosophies that influenced the creation of the Marten web framework. It seeks to provide insight into the past and serve as a reference for the future. + +## Simple and easy to use + +Marten tries to ensure that everything it enables is as simple as possible and that the syntax provided for dealing with the framework's components remains obvious and easy to remember (and certainly not complex or obscure). The framework makes it as easy as possible to leverage its capabilities and perform CRUD operations. + +## Full-featured + +Marten adheres to the "batteries included" philosophy. Out of the box, it provides the tools and features that are commonly required by web applications: [ORM](../models-and-databases/introduction.md), [migrations](../models-and-databases/migrations.md), [translations](../i18n/introduction.md), [templating engine](../templates/introduction.md), [sessions](../handlers-and-http/sessions.md), [emailing](../emailing/introduction.md), and [authentication](../authentication/introduction.md). + +## Extensible + +Marten gives developers the ability to contribute extra functionalities to the framework easily. Things like [custom model field implementations](../models-and-databases/how-to/create-custom-model-fields.md), [new route parameter types](../handlers-and-http/how-to/create-custom-route-parameters.md), [session stores](../handlers-and-http/sessions.md#session-stores), etc... can all be registered to the framework easily. + +## DB-Neutral + +The framework's ORM is and should remain usable with multiple database backends (including MariaDB, MySQL, PostgreSQL, and SQLite). + +## App-oriented + +Marten allows separating projects into a set of logical "[apps](../development/applications.md)", which helps improve code organization and makes it easy for multiple developers to work on different components. Each app can contribute specific abstractions and features to a project like models and migrations, templates, HTTP handlers and routes, etc. These apps can also be extracted in Crystal shards in order to contribute features and behaviors to other Marten projects. The goal behind this capability is to allow the creation of a powerful apps ecosystem over time and to encourage "reusability" and "pluggability". + +:::tip +In this light, the [Awesome Marten](https://github.com/martenframework/awesome-marten) repository lists applications that you can leverage in your projects. +::: + +## Backend-oriented + +The framework is intentionally very "backend-oriented" because the idea is to not make too many assumptions regarding how the frontend code and assets should be structured, packaged or bundled together. The framework can't account for all the ways assets can be packaged and/or bundled together and does not advocate for specific solutions in this area. Some projects might require a webpack strategy to bundle assets, some might require a fingerprinting step on top of that, and others might need something entirely different. How these toolchains are configured or set up is left to the discretion of web application developers, and the framework simply makes it easy to [reference these assets](../assets/introduction.md) and [collect them](../assets/introduction.md#serving-assets-in-production) at deploy time to upload them to their final destination. diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes.md similarity index 54% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes.md index 21b91ecb3..7450a11d6 100644 --- a/docs/versioned_docs/version-0.2/the-marten-project/release-notes.md +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes.md @@ -6,6 +6,27 @@ pagination_next: null Here are listed the release notes for each version of the Marten web framework. +## Marten 0.5 + +* [Marten 0.5 release notes](./release-notes/0.5.md) + +## Marten 0.4 + +* [Marten 0.4.5 release notes](./release-notes/0.4.5.md) +* [Marten 0.4.4 release notes](./release-notes/0.4.4.md) +* [Marten 0.4.3 release notes](./release-notes/0.4.3.md) +* [Marten 0.4.2 release notes](./release-notes/0.4.2.md) +* [Marten 0.4.1 release notes](./release-notes/0.4.1.md) +* [Marten 0.4 release notes](./release-notes/0.4.md) + +## Marten 0.3 + +* [Marten 0.3.4 release notes](./release-notes/0.3.4.md) +* [Marten 0.3.3 release notes](./release-notes/0.3.3.md) +* [Marten 0.3.2 release notes](./release-notes/0.3.2.md) +* [Marten 0.3.1 release notes](./release-notes/0.3.1.md) +* [Marten 0.3 release notes](./release-notes/0.3.md) + ## Marten 0.2 * [Marten 0.2.4 release notes](./release-notes/0.2.4.md) diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.1.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.1.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.1.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.1.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.2.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.2.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.2.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.2.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.3.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.3.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.3.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.3.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.4.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.4.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.4.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.4.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.5.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.5.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.5.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.5.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.1.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.1.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.1.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.1.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.1.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.1.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.2.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.2.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.2.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.2.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.3.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.3.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.3.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.3.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.4.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.4.md similarity index 100% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.4.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.4.md diff --git a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.md similarity index 98% rename from docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.md rename to docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.md index 039c9b614..434c74ee1 100644 --- a/docs/versioned_docs/version-0.2/the-marten-project/release-notes/0.2.md +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.2.md @@ -73,7 +73,7 @@ end ### Transaction callbacks -Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#aftercommit) and [`#after_rollback`](../../models-and-databases/callbacks.md#afterrollback) macros. +Models now support the definition of transaction callbacks by using the [`#after_commit`](../../models-and-databases/callbacks.md#after_commit) and [`#after_rollback`](../../models-and-databases/callbacks.md#after_rollback) macros. For example: diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.1.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.1.md new file mode 100644 index 000000000..2e901880c --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.1.md @@ -0,0 +1,13 @@ +--- +title: Marten 0.3.1 release notes +pagination_prev: null +pagination_next: null +--- + +_July 10, 2023._ + +## Bug fixes + +* Ensure that context objects provided by [generic handlers](../../handlers-and-http/generic-handlers.md) are initialized using [`Marten::Template::Context`](pathname:///api/0.5/Marten/Template/Context.html). +* Fix a possible compilation error happening around template variables initialization. +* Ensure that `#?` schema methods return `false` for empty field values. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.2.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.2.md new file mode 100644 index 000000000..5feb3f9b2 --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.2.md @@ -0,0 +1,14 @@ +--- +title: Marten 0.3.2 release notes +pagination_prev: null +pagination_next: null +--- + +_July 23, 2023._ + +## Bug fixes + +* Fix possible inconsistencies in results returned by query sets based on the order of calls to [`#filter`](../../models-and-databases/reference/query-set.md#filter) and [`#exclude`](../../models-and-databases/reference/query-set.md#exclude). +* Fix invalid through model generation for recursive [many-to-many relationships](../../models-and-databases/introduction.md#many-to-many-relationships). +* Ensure that `#?` model methods return false for empty field values. +* Add missing `#?` method for [`file`](../../models-and-databases/reference/fields.md#file) model fields. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.3.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.3.md new file mode 100644 index 000000000..b260e325c --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.3.md @@ -0,0 +1,14 @@ +--- +title: Marten 0.3.3 release notes +pagination_prev: null +pagination_next: null +--- + +_September 15, 2023._ + +## Bug fixes + +* Fix unexpected [`Marten::Template::Errors::InvalidSyntax`](pathname:///api/0.3/Marten/Template/Errors/InvalidSyntax.html) exceptions raised when adding spaces between a [template filter](../../templates/introduction.md#filters) name and its argument. +* Make sure that the [`#add`](pathname:///api/0.3/Marten/DB/Query/ManyToManySet.html#add(*objs:M)-instance-method) method of many-to-many query sets honors the targeted DB alias. +* Make sure that the [`#delete`](../../models-and-databases/reference/query-set.md#delete) and [`#update`](../../models-and-databases/reference/query-set.md#update) query set methods reset cached records if applicable. +* Make sure that the [`#add`](pathname:///api/0.3/Marten/DB/Query/ManyToManySet.html#add(*objs:M)-instance-method) method of many-to-many query sets resets cached records if applicable. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.4.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.4.md new file mode 100644 index 000000000..3a5fcfe5f --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.4.md @@ -0,0 +1,11 @@ +--- +title: Marten 0.3.4 release notes +pagination_prev: null +pagination_next: null +--- + +_January 9, 2024._ + +## Bug fixes + +* Fix compilation error with Crystal 1.11. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.md new file mode 100644 index 000000000..3bb211034 --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.3.md @@ -0,0 +1,153 @@ +--- +title: Marten 0.3.0 release notes +pagination_prev: null +pagination_next: null +--- + +_June 19, 2023._ + +## Requirements and compatibility + +Crystal 1.6, 1.7, and 1.8. + +## New features + +### Support for streaming responses + +It is now possible to generate streaming responses from iterators of strings easily by leveraging the [`Marten::HTTP::Response::Streaming`](pathname:///api/0.3/Marten/HTTP/Response/Streaming.html) class or the [`#respond`](pathname:///api/0.3/Marten/Handlers/Base.html#respond(streamed_content%3AIterator(String)%2Ccontent_type%3DHTTP%3A%3AResponse%3A%3ADEFAULT_CONTENT_TYPE%2Cstatus%3D200)-instance-method) helper method. This can be beneficial if you intend to generate lengthy responses or responses that consume excessive memory (a classic example of this is the generation of large CSV files). + +Please refer to [Streaming responses](../../handlers-and-http/introduction.md#streaming-responses) to learn more about this new capability. + +### Caching + +Marten now lets you interact with a global cache store that allows interacting with an underlying cache system and performing basic operations such as fetching cached entries, writing new entries, etc. By using caching, you can save the result of expensive operations so that you don't have to perform them for every request. + +The global cache can be accessed by leveraging the [`Marten#cache`](pathname:///api/0.3/Marten.html#cache%3ACache%3A%3AStore%3A%3ABase-class-method) method. Here are a few examples on how to perform some basic caching operations: + +```crystal +# Fetching an entry from the cache. +Marten.cache.fetch("mykey", expires_in: 4.hours) do + "myvalue" +end + +# Reading from the cache. +Marten.cache.read("unknown") # => nil +Marten.cache.read("mykey") # => "myvalue" +Marten.cache.exists?("mykey") => true + +# Writing to the cache. +Marten.cache.write("foo", "bar", expires_in: 10.minutes) => true +``` + +Marten's caching leverages a [cache store mechanism](../../caching/introduction.md#configuration-and-cache-stores). By default, Marten uses an in-memory cache (instance of [`Marten::Cache::Store::Memory`](pathname:///api/0.3/Marten/Cache/Store/Memory.html)) and other [third-party stores](../../caching/reference/stores.md#other-stores) can be installed depending on your caching requirements (eg. Memcached, Redis). + +Marten's new caching capabilities are not only limited to its standard cache functionality. They can also be effectively utilized via the newly introduced [template fragment caching](../../caching/introduction.md#template-fragment-caching) feature, made possible by the [`cache`](../../templates/reference/tags.md#cache) template tag. With this feature, specific parts of your [templates](../../templates.mdx) can now be cached with ease. + +Please refer to the [Caching](../../caching.mdx) to learn more about these new capabilities. + +### JSON field for models and schemas + +Marten now provides the ability to define `json` fields in [models](../../models-and-databases/reference/fields.md#json) and [schemas](../../schemas/reference/fields.md#json). These fields allow you to easily persist and interact with valid JSON structures that are exposed as [`JSON::Any`](https://crystal-lang.org/api/JSON/Any.html) objects by default. + +For example: + +```crystal +class MyModel < Marten::Model + # Other fields... + field :metadata, :json +end + +MyModel.last!.metadata # => JSON::Any object +``` + +Additionally, it is also possible to specify that JSON values must be deserialized using a class that makes use of [`JSON::Serializable`](https://crystal-lang.org/api/JSON/Serializable.html). This can be done by leveraging the `serializable` option in both [model fields](../../models-and-databases/reference/fields.md#json) and [schema fields](../../schemas/reference/fields.md#serializable). + +For example: + +```crystal +class MySerializable + include JSON::Serializable + + property a : Int32 | Nil + property b : String | Nil +end + +class MyModel < Marten::Model + # Other fields... + field :metadata, :json, serializable: MySerializable +end + +MyModel.last!.metadata # => MySerializable object +``` + +### Duration field for models and schemas + +It is now possible to define `duration` fields in [models](../../models-and-databases/reference/fields.md#duration) and [schemas](../../schemas/reference/fields.md#duration). These allow you to easily persist valid durations (that map to [`Time::Span`](https://crystal-lang.org/api/Time/Span.html) objects in Crystal) in your models but also to expect valid durations in data validated through the use of schemas. + +For example: + +```crystal +class Recipe < Marten::Model + field :id, :big_int, primary_key: true, auto: true + # Other fields... + field :fridge_time, :duration, blank: true, null: true +end +``` + +### Minor features + +#### Models and databases + +* New [`#get_or_create`](../../models-and-databases/reference/query-set.md#get_or_create) / [`#get_or_create!`](../../models-and-databases/reference/query-set.md#get_or_create-1) methods were added to query sets in order to allow easily retrieving a model record matching a given set of filters or creating a new one if no record is found. +* [`string`](../../models-and-databases/reference/fields.md#string) fields now support a `min_size` option allowing to validate the minimum size of persisted string field values. +* A new [`#includes?`](../../models-and-databases/reference/query-set.md#includes) method was added to query sets in order easily perform membership checks without loading the entire list of records targeted by a given query set. +* Alternative [`#exists?`](../../models-and-databases/reference/query-set.md#exists) methods were added to query sets in order to allow specifying additional filters to use as part of existence checks. +* An [`#any?`](pathname:///api/0.3/Marten/DB/Query/Set.html#any%3F-instance-method) method was added to query sets in order to short-circuit the default implementation of [`Enumerable#any?`](https://crystal-lang.org/api/Enumerable.html#any%3F%3ABool-instance-method) and to avoid loading the full list of records in memory (when called without arguments). This overridden method is technically an alias of [`#exists?`](../../models-and-databases/reference/query-set.md#exists). +* Marten migrations are now optimized to prevent possible issues with circular dependencies within added or deleted tables +* It is now possible to define arbitrary database options by using the new [`db.options`](../../development/reference/settings.md#options) database setting. +* It is now possible to define [`many_to_one`](../../models-and-databases/reference/fields.md#many_to_one) and [`one_to_one`](../../models-and-databases/reference/fields.md#one_to_one) fields that target models with non-integer primary key fields (such as UUID fields for example). + +#### Handlers and HTTP + +* The [`Marten::Handlers::RecordList`](../../handlers-and-http/reference/generic-handlers.md#listing-records) generic record now provides the ability to specify a custom [query set](../../models-and-databases/queries.md) instead of a model class. This can be achieved through the use of the [`#queryset`](pathname:///api/0.3/Marten/Handlers/RecordListing.html#queryset(queryset)-macro) macro. +* A new [`Marten::Middleware::AssetServing`](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) middleware was introduced to make it easy to serve collected assets in situations where it is not possible to easily configure a web server (such as [Nginx](https://nginx.org)) or a third-party service (like Amazon's S3 or GCS) to serve assets directly. +* A new [`Marten::Middleware::SSLRedirect`](../../handlers-and-http/reference/middlewares.md#ssl-redirect-middleware) middleware was introduced to allow redirecting non-HTTPS requests to HTTPS easily. +* A new [`Marten::Middleware::ContentSecurityPolicy`](../../handlers-and-http/reference/middlewares.md#content-security-policy-middleware) middleware was introduced to ensure the presence of the Content-Security-Policy header in the response's headers. Please refer to [Content Security Policy](../../security/content-security-policy.md) to learn more about the Content-Security-Policy header and how to configure it. +* The [`Marten::Middleware::I18n`](../../handlers-and-http/reference/middlewares.md#i18n-middleware) middleware can now automatically determine the current locale based on the value of a cookie whose name can be configured with the [`i18n.locale_cookie_name`](../../development/reference/settings.md#locale_cookie_name) setting. +* The [`Marten::Middleware::I18n`](../../handlers-and-http/reference/middlewares.md#i18n-middleware) middleware now automatically sets the Content-Language header based on the activated locale. + +#### Templates + +* A [`join`](../../templates/reference/filters.md#join) template filter was introduced to allow converting enumerable template values into a string separated by a separator value. +* A [`split`](../../templates/reference/filters.md#split) template filter was introduced to allow converting a string into an array of elements. + +#### Schemas + +* Type-safe getter methods (ie. `#`, `#!`, and `#?`) are now automatically generated for schema fields. Please refer to [Accessing validated data](../../schemas/introduction.md#accessing-validated-data) in the schemas documentation to read more about these methods and how/why to use them. + +#### Development + +* [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.3/Marten/HTTP/Errors/SuspiciousOperation.html) exceptions are now showcased using the debug internal error page handler to make it easier to diagnose errors such as unexpected host errors (which result from a missing host value in the [`allowed_hosts`](../../development/reference/settings.md#allowed_hosts) setting). +* [`Marten#setup`](pathname:///api/0.3/Marten.html#setup-class-method) now raises.[`Marten::Conf::Errors::InvalidConfiguration`](pathname:///api/0.3/Marten/Conf/Errors/InvalidConfiguration.html) exceptions when a configured database involves a backend that is not installed (eg. a MySQL database configured without `crystal-lang/crystal-mysql` installed and required). +* The [`new`](../../development/reference/management-commands.md#new) management command now automatically creates a [`.editorconfig`](https://editorconfig.org) file for new projects. +* A new [`root_path`](../../development/reference/settings.md#root_path) setting was introduced to make it possible to configure the actual location of the project sources in your system. This is especially useful when deploying projects that have been compiled in a different location from their final destination, which can happen on platforms like Heroku. By setting the root path, you can ensure that your application can find all necessary project sources, as well as other files like locales, assets, and templates. +* A new `--plan` option was added to the [`migrate`](../../development/reference/management-commands.md#migrate) management command in order to provide a comprehensive overview of the operations that will be performed by the applied or unapplied migrations. +* An interactive mode was added to the [`new`](../../development/reference/management-commands.md#new) management command: if the `type` and `name` arguments are not provided, the command now prompts the user for inputting the structure type, the app or project name, and whether the auth app should be generated. +* It is now possible to specify command aliases when defining management commands by leveraging the [`#command_aliases`](pathname:///api/0.3/Marten/CLI/Manage/Command/Base.html#command_aliases(*aliases%3AString|Symbol)-class-method) helper method. + +#### Security + +* The ability to fully configure and customize the Content-Security-Policy header was added to the framework. Please refer to [Content Security Policy](../../security/content-security-policy.md) to learn more about the Content-Security-Policy header and how to configure it in Marten projects. + +#### Deployment + +* A new guide was added in order to document [how to deploy on Heroku](../../deployment/how-to/deploy-to-heroku). +* A new guide was added in order to document [how to deploy on Fly.io](../../deployment/how-to/deploy-to-fly-io). + +## Backward incompatible changes + +### Handlers and HTTP + +* [Custom route parameter](../../handlers-and-http/how-to/create-custom-route-parameters.md) must now implement a [`#regex`](pathname:///api/0.3/Marten/Routing/Parameter/Base.html#regex%3ARegex-instance-method) method and can no longer rely on a `#regex` macro to generate such method. +* The generic handlers that used to require the use of a `#model` class method now leverage a dedicated macro instead. This is to make handlers that inherit from generic handler classes more type-safe when it comes to manipulating model records. +* The generic handlers that used to require the use of a `#schema` class method now leverage a dedicated macro instead. This is to make handlers that inherit from generic handler classes more type-safe when it comes to manipulating schema instances. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.1.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.1.md new file mode 100644 index 000000000..190a373ba --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.1.md @@ -0,0 +1,11 @@ +--- +title: Marten 0.4.1 release notes +pagination_prev: null +pagination_next: null +--- + +_February 20, 2024._ + +## Bug fixes + +* Fix the position of the `config/routes.cr` file requirement in the `src/project.cr` file of generated projects. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.2.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.2.md new file mode 100644 index 000000000..40364a0e6 --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.2.md @@ -0,0 +1,11 @@ +--- +title: Marten 0.4.2 release notes +pagination_prev: null +pagination_next: null +--- + +_February 27, 2024._ + +## Bug fixes + +* Fix a possible syntax error in the SQL queries generated for SQLite databases when running migrations. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.3.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.3.md new file mode 100644 index 000000000..723736092 --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.3.md @@ -0,0 +1,15 @@ +--- +title: Marten 0.4.3 release notes +pagination_prev: null +pagination_next: null +--- + +_March 10, 2024._ + +## Bug fixes + +* Ensure that request query parameters can be accessed from templates. +* Fix hidden folders being ignored by the [`collectassets`](../../development/reference/management-commands.md#collectassets) management command. +* Fix some ameba warnings in generated projects and applications. +* Fix inconsistency of `#using` method definitions between model classes and query sets. +* Fix missing DB-specific requirements in generated projects. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.4.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.4.md new file mode 100644 index 000000000..48b980cd4 --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.4.md @@ -0,0 +1,11 @@ +--- +title: Marten 0.4.4 release notes +pagination_prev: null +pagination_next: null +--- + +_April 4, 2024._ + +## Bug fixes + +* Fix possible SQL syntax errors occurring when using [`in`](../../models-and-databases/reference/query-set.md#in) predicates with empty arrays. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.5.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.5.md new file mode 100644 index 000000000..64267f06b --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.5.md @@ -0,0 +1,13 @@ +--- +title: Marten 0.4.5 release notes +pagination_prev: null +pagination_next: null +--- + +_May 5, 2024._ + +## Bug fixes + +* Fix a possible compilation error with custom settings namespace generation. +* Ensure that the [`serve`](../../development/reference/management-commands.md#serve) management command recompiles projects upon translation file changes. +* Fix the order of asset finders to ensure that those associated with configured asset directories (via the [`assets.dirs`](../../development/reference/settings.md#dirs) setting) have priority over those associated with application directories. diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.md new file mode 100644 index 000000000..434e30518 --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.4.md @@ -0,0 +1,174 @@ +--- +title: Marten 0.4.0 release notes +pagination_prev: null +pagination_next: null +--- + +_January 13, 2024._ + +## Requirements and compatibility + +Crystal 1.9, 1.10, and 1.11. + +## New features + +### Generators + +Marten now provides a generator mechanism that makes it easy to create various abstractions, files, and structures within an existing project. This feature is available through the use of the [`gen`](../../development/reference/management-commands.md#gen) management command and facilitates the generation of key components such as [models](../../models-and-databases/introduction.md), [schemas](../../schemas/introduction.md), [emails](../../emailing/introduction.md), or [applications](../../development/applications.md). The [authentication application](../../authentication/introduction.md) can now also be added easily to existing projects through the use of generators. By leveraging generators, developers can improve their workflow and speed up the development of their Marten projects while following best practices. + +Below are highlighted some examples illustrating the use of the [`gen`](../../development/reference/management-commands.md#gen) management command: + +```sh +# Generate a model in the admin app: +marten gen model User name:string email:string --app admin + +# Generate a new TestEmail email in the blog application: +marten gen email TestEmail --app blog + +# Add a new 'blogging' application to the current project: +marten gen app blogging + +# Add the authentication application to the current project: +margen gen auth +``` + +You can read more about the generator mechanism in the [dedicated documentation](../../development/generators.md). All the available generators are also listed in the [generators reference](../../development/reference/generators.md). + +### Multi table inheritance + +It is now possible to define models that inherit from other concrete models (ie. non-abstract models). In this situation, each model can be used/queried individually and has its own associated database table. The framework automatically defines a set of "links" between each model that uses multi table inheritance and its parent models in order to ensure that the relational structure and inheritance hierarchy are maintained. + +For example: + +```crystal +class Person < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :first_name, :string, max_size: 100 + field :last_name, :string, max_size: 100 +end + +class Employee < Person + field :company_name, :string, max_size: 100 +end + +employee = Employee.filter(first_name: "John").first! +employee.first_name # => "John" +``` + +All the fields defined in the `Person` model can be accessed when interacting with records of the `Employee` model (despite the fact that the data itself is stored in distinct tables). + +You can read more about this new kind of model inheritance in [Multi table inheritance](../../models-and-databases/introduction.md#multi-table-inheritance). + +### Schema handler callbacks + +Handlers that inherit from the base schema handler - [`Marten::Handlers::Schema`](pathname:///api/0.4/Marten/Handlers/Schema.html) - or one of its subclasses (such as [`Marten::Handlers::RecordCreate`](pathname:///api/0.4/Marten/Handlers/RecordCreate.html) or [`Marten::Handlers::RecordUpdate`](pathname:///api/0.4/Marten/Handlers/RecordUpdate.html)) can now define new kinds of callbacks that allow to easily manipulate the considered [schema](../../schemas/introduction.md) instance and to define logic to execute before the schema is validated or after (eg. when the schema validation is successful or failed): + +* [`before_schema_validation`](../../handlers-and-http/callbacks.md#before_schema_validation) +* [`after_schema_validation`](../../handlers-and-http/callbacks.md#after_schema_validation) +* [`after_successful_schema_validation`](../../handlers-and-http/callbacks.md#after_successful_schema_validation) +* [`after_failed_schema_validation`](../../handlers-and-http/callbacks.md#after_failed_schema_validation) + +For example, the [`after_successful_schema_validation`](../../handlers-and-http/callbacks.md#after_successful_schema_validation) callback can be used to create a flash message after a schema has been successfully validated: + +```crystal +class ArticleCreateHandler < Marten::Handlers::Schema + success_route_name "home" + template_name "articles/create.html" + schema ArticleSchema + + after_successful_schema_validation :generate_success_flash_message + + private def generate_success_flash_message : Nil + flash[:notice] = "Article successfully created!" + end +end +``` + +Please head over to [Schema handler callbacks](../../handlers-and-http/callbacks.md#schema-handler-callbacks) to learn more about these new types of callbacks. + +### URL field for models and schemas + +It is now possible to define `url` fields in [models](../../models-and-databases/reference/fields.md#url) and [schemas](../../schemas/reference/fields.md#url). These allow you to easily persist valid URLs in your models but also to expect valid URL values in data validated through the use of schemas. + +For example: + +```crystal +class User < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :website_url, :url, blank: true, null: true +end +``` + +### Slug field for models and schemas + +It is now possible to define `slug` fields in [models](../../models-and-databases/reference/fields.md#slug) and [schemas](../../schemas/reference/fields.md#slug). These allow you to easily persist valid slug values (ie. strings that can only include characters, numbers, dashes, and underscores) in your models but also to expect such values in data validated through the use of schemas. + +For example: + +```crystal +class User < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :username, :slug +end +``` + +### Minor features + +#### Models and databases + +* Support for removing records from many-to-many fields was added and many-to-many field query sets now provide a [`#remove`](pathname:///api/0.4/Marten/DB/Query/ManyToManySet.html#remove(*objs%3AM)%3ANil-instance-method) helper method allowing to easily remove specific records from a specific relation. You can learn more about this capability in [Many-to-many relationships](../../models-and-databases/relationships.md#many-to-many-relationships). +* Support for clearing all the references to records targeted by many-to-many fields was added. Indeed, many-to-many field query sets now provide a [`#clear`](pathname:///api/0.4/Marten/DB/Query/ManyToManySet.html#clear%3ANil-instance-method) method allowing to easily clear a specific relation. You can learn more about this capability in [Many-to-many relationships](../../models-and-databases/relationships.md#many-to-many-relationships). +* It is now possible to specify arrays of records to add or remove from a many-to-many relationship query set, through the use of the [`#add`](pathname:///api/0.4/Marten/DB/Query/ManyToManySet.html#add(*objs%3AM)-instance-method) and [`#remove`](pathname:///api/0.4/Marten/DB/Query/ManyToManySet.html#remove(*objs%3AM)%3ANil-instance-method) methods. See the [related documentation](../../models-and-databases/relationships.md#interacting-with-related-records-2) to learn more about interacting with records targeted by many-to-many relationships. +* Records targeted by reverse relations that are contributed to models by [`one_to_one`](../../models-and-databases/reference/fields.md#one_to_one) (ie. when using the [`related`](../../models-and-databases/reference/fields.md#related-2) option) are now memoized when the corresponding methods are called on related model instances. +* Relation fields that contribute methods that return query sets to models (such as [`many_to_one`](../../models-and-databases/reference/fields.md#many_to_one) or [`many_to_many`](../../models-and-databases/reference/fields.md#many_to_many) fields) now make sure that those query set objects are memoized at the record level. The corresponding instance variables are also reset when the considered records are reloaded. This allows to limit the number of queries involved when iterating multiple times over the records targeted by a [`many_to_many`](../../models-and-databases/reference/fields.md#many_to_many) field for example. +* The [`#order`](../../models-and-databases/reference/query-set.md#order) query set method can now be called directly on model classes to allow retrieving all the records of the considered model in a specific order. +* A [`#pk?`](pathname:///api/0.4/Marten/DB/Model/Table.html#pk%3F%3ABool-instance-method) model method can now be leveraged to determine if a primary key value is set on a given model record. +* The [`#join`](../../models-and-databases/reference/query-set.md#join) query set method now makes it possible to pre-select one-to-one reverse relations. This essentially allows to traverse a [`one_to_one`](../../models-and-databases/reference/fields.md#one_to_one) field back to the model record on which the field is specified. +* The [`#count`](../../models-and-databases/reference/query-set.md#count) query set method can now take an optional field name to count the number of records that have a non-null value for the corresponding column in the database. + +#### Handlers and HTTP + +* It is now optional to define a name for included route maps (but defining a name for individual routes that are associated with [handlers](../../handlers-and-http/introduction.md) is still mandatory). You can read more about this in [Defining included routes](../../handlers-and-http/routing.md#defining-included-routes). +* The [`Marten::Handlers::Schema`](pathname:///api/0.4/Marten/Handlers/Schema.html) generic handler now allows modifying the schema object context name through the use of the [`#schema_context_name`](pathname:///api/0.4/Marten/Handlers/Schema.html#schema_context_name(name%3AString|Symbol)-class-method) method. +* It is now possible to specify symbol status codes when making use of the [`#respond`](../../handlers-and-http/introduction.md#respond), [`#render`](../../handlers-and-http/introduction.md#render), [`#head`](../../handlers-and-http/introduction.md#head), and [`#json`](../../handlers-and-http/introduction.md#json) response helper methods. Such symbols must comply with the values of the [`HTTP::Status`](https://crystal-lang.org/api/HTTP/Status.html) enum. +* The hash of matched routing parameters that is available from handlers through the use of the `#params` method can accept symbols and strings when performing key lookups. +* The [GZip middleware](../../handlers-and-http/reference/middlewares.md#gzip-middleware) now incorporates a mitigation strategy against the BREACH attack. This strategy (described in the [Heal The Breach paper](https://ieeexplore.ieee.org/document/9754554)) involves introducing up to 100 random bytes into GZip responses to enhance the security against such attacks. +* A new [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback type is now available to handlers. Such callbacks are executed before rendering a [template](../../templates/introduction.md) in order to produce a response. As such they are well suited for adding new variables to the global template context so that they are available to the template runtime. +* All handlers now have access to a [global template context](../../handlers-and-http/introduction.md#global-template-context) through the use of the [`#context`](pathname:///api/0.4/Marten/Handlers/Base.html#context-instance-method) method. This template context object is available for the lifetime of the considered handler and can be mutated to define which variables are made available to the template runtime when rendering templates (either through the use of the [`#render`](../../handlers-and-http/introduction.md#render) helper method or when rendering templates as part of subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/generic-handlers.md#rendering-a-template) generic handler). This feature can be combined with the [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback to effortlessly introduce new variables to the context used for rendering a template and generating a handler response. + +#### Templates + +* A [`with`](../../templates/reference/tags.md#with) template tag was introduced in order to make it easy to assign one or more variables inside a template block. +* A [`time`](../../templates/reference/filters.md#time) template tag was introduced in order to make it possible to output the string representation of a time variable according to a specific [time format pattern](https://crystal-lang.org/api/Time/Format.html). +* An [`escape`](../../templates/reference/filters.md#escape) template tag was introduced in order to make it easy to explicitly escape [safe strings](../../templates/introduction.md#auto-escaping) in templates. +* The ability to configure how undefined/unknown variables are treated was added to the framework: by default, such variables are treated as `nil` values (so nothing is displayed for such variables, and they are evaluated as falsey in if conditions). This behavior can be configured via the [`templates.strict_variables`](../../development/reference/settings.md#strict_variables) setting, and you can learn more about it in [Strict variables](../../templates/introduction.md#strict-variables). + +#### Development + +* The [`new`](../../development/reference/management-commands.md#new) management command now accepts an optional `--database` option that can be used to preconfigure the application database (eg. `--database=postgresql`). +* A [`clearsessions`](../../development/reference/management-commands.md#clearsessions) management command was introduced in order to ease the process of clearing expired session entries. +* Custom management commands can now define how they want to handle unknown or undefined arguments through the use of the [`#on_unknown_argument`](pathname:///api/0.4/Marten/CLI/Manage/Command/Base.html#on_unknown_argument(%26block%3AString->)-instance-method) method. This can be leveraged to implement management commands which can accept a variable number of positional arguments. +* Custom management commands can now define how they want to handle invalid options through the use of the [`#on_invalid_option`](pathname:///api/0.4/Marten/CLI/Manage/Command/Base.html#on_invalid_option(%26block%3AString->)-instance-method) method. + +#### Emailing + +* [Emails](../../emailing/introduction.md) now provide a set of [callbacks](../../emailing/callbacks.md) that make it easy to define logic that is triggered at different stages of an email's lifecycle (before/after an email gets delivered, before rendering the email's template). + +#### Authentication + +* The generated authentication application now features the ability to change the password of the currently logged-in user. + +## Backward incompatible changes + +### Handlers and HTTP + +* Custom [session stores](../../handlers-and-http/sessions.md#session-stores) must now implement a [`#clear_expired_entries`](pathname:///api/0.4/Marten/HTTP/Session/Store/Base.html#clear_expired_entries%3ANil-instance-method) method (allowing to clear expired session entries if this is applicable for the considered store). +* The introduction of the [global template context](../../handlers-and-http/introduction.md#global-template-context) involves that generic handlers that used to override the `#context` method (in order to insert record or schema objects into the template context for example) now leverage [`before_render`](../../handlers-and-http/callbacks.md#before_render) callbacks in order to mutate the global context and define the same variables. Generic handler subclasses that were overriding this `#context` method and calling `super` in it will likely need to be updated in order to leverage the [`before_render`](../../handlers-and-http/callbacks.md#before_render) callback to add custom variables to the [global template context](../../handlers-and-http/introduction.md#global-template-context). + +### Templates + +* The [`default`](../../templates/reference/filters.md#default) template filter will now return the specified default value if the incoming value is falsey or empty. + +### Emailing + +* The introduction of the [global template context](../../emailing/introduction.md#modifying-the-template-context) involves that emails that used to explicitly define a `#context` method (eg. in order to define a hash of template variables from local instance variables) won't work anymore. Instead these emails should now leverage the [`before_render`](../../emailing/callbacks.md#before_render) callback in order to [add these variables to the email template context object](../../emailing/introduction.md#modifying-the-template-context). diff --git a/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.5.md b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.5.md new file mode 100644 index 000000000..7e4dedfba --- /dev/null +++ b/docs/versioned_docs/version-0.5/the-marten-project/release-notes/0.5.md @@ -0,0 +1,171 @@ +--- +title: Marten 0.5.0 release notes +pagination_prev: null +pagination_next: null +--- + +_July 13, 2024._ + +## Requirements and compatibility + +* **Crystal:** 1.11, 1.12, and 1.13. +* **Databases:** + * MariaDB 10.4 and higher. + * MySQL 8.0.11 and higher. + * PostgreSQL 12 and higher. + * SQLite 3.27.0 and higher. + +## New features + +### Relations pre-fetching + +Marten now provides the ability to prefetch relations when using [query sets](../../models-and-databases/queries.md) through the use of the new [`#prefetch`](../../models-and-databases/reference/query-set.md#prefetch) method. When using [`#prefetch`](../../models-and-databases/reference/query-set.md#prefetch), the records corresponding to the specified relationships will be prefetched in single batches and each record returned by the original query set will have the corresponding related objects already selected and populated. + +For example: + +```crystal +posts_1 = Post.all.to_a +# hits the database to retrieve the related "tags" (many-to-many relation) +puts posts_1[0].tags.to_a + +posts_2 = Post.all.prefetch(:tags).to_a +# doesn't hit the database since the related "tags" relation was already prefetched +puts posts_2[0].tags.to_a +``` + +Like the existing [`#join`](../../models-and-databases/reference/query-set.md#join) method, this allows to alleviate N+1 issues commonly encountered when accessing related objects. However, unlike [`#join`](../../models-and-databases/reference/query-set.md#join) (which can only be used with single-valued relationships), [`#prefetch`](../../models-and-databases/reference/query-set.md#prefetch) can be used with both single-valued relationships and multi-valued relationships (such as [many-to-many](../../models-and-databases/relationships.md#many-to-many-relationships) relationships, [reverse many-to-many](../../models-and-databases/relationships.md#backward-relations-2) relationships, and [reverse many-to-one](../../models-and-databases/relationships.md#backward-relations) relationships). + +Please refer to [Pre-fetching relations](../../models-and-databases/queries.md#pre-fetching-relations) to learn more about this new capability. + +### Model scopes + +It is now possible to define [scopes](../../models-and-databases/queries.md#scopes) in model classes. Scopes allow to pre-define specific filtered query sets, which can be easily applied to model classes and model query sets. + +Such scopes can be defined through the use of the [`#scope`](pathname:///api/0.5/Marten/DB/Model/Querying.html#scope(name%2C%26block)-macro) macro, which expects a scope name (string literal or symbol) as first argument and requires a block where the query set filtering logic is defined: + +```crystal +class Post < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :is_published, :bool, default: false + field :created_at, :date_time + + // highlight-next-line + scope :published { filter(is_published: true) } + // highlight-next-line + scope :unpublished { filter(is_published: false) } + // highlight-next-line + scope :recent { filter(created_at__gt: 1.year.ago) } +end + +Post.published # => Post::QuerySet [...]> +``` + +It is also possible to override the default scope through the use of the [`#default_scope`](pathname:///api/0.5/Marten/DB/Model/Querying.html#default_scope-macro) macro. This macro requires a block where the query set filtering logic is defined: + +```crystal +class Post < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 + field :is_published, :bool, default: false + field :created_at, :date_time + + // highlight-next-line + default_scope { filter(is_published: true) } +end +``` + +Please refer to [Scopes](../../models-and-databases/queries.md#scopes) for more details on how to define scopes. + +### Enum field for models and schemas + +It is now possible to define `enum` fields in [models](../../models-and-databases/reference/fields.md#enum) and [schemas](../../schemas/reference/fields.md#enum). For models, such fields allow you to store valid enum values, with validation enforced at the database level. When validating data with schemas, they allow you to expect valid string values that match those of the configured enum. + +For example: + +```crystal +enum Category + NEWS + BLOG +end + +class Article < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :category, :enum, values: Category +end + +article = Article.last! +article.category # => Category::BLOG +``` + +### Raw SQL predicate filtering + +Marten now provides the ability to filter [query sets](../../models-and-databases/queries.md) using [raw SQL predicates](../../models-and-databases/raw-sql.md#filtering-with-raw-sql-predicates) through the use of the `#filter` method. This is useful when you want to leverage the flexibility of SQL for specific conditions, but still want Marten to handle the column selection and query building for the rest of the query. + +For example: + +```crystal +Author.filter("first_name = :first_name", first_name: "John") +Author.filter("first_name = ?", "John") +Author.filter { q("first_name = :first_name", first_name: "John") } +``` + +Please refer to [Filtering with raw SQL predicates](../../models-and-databases/raw-sql.md#filtering-with-raw-sql-predicates) to learn more about this new capability. + +### Minor features + +#### Models and databases + +* A new [`#pks`](../../models-and-databases/reference/query-set.md#pks) method was introduced for query sets to make it easy to retrieve the primary key values of the model records targeted by a given current query set. +* A [`#count`](pathname:///api/0.5/Marten/DB/Model/Querying/ClassMethods.html#count(field%3AString|Symbol|Nil%3Dnil)-instance-method) method is now available on model classes and provides the same functionality as the [`#count`](../../models-and-databases/reference/query-set.md#count) query set method. +* A new [`#bulk_create`](../../models-and-databases/reference/query-set.md#bulk_create) method was introduced to make it easy to insert multiple model instances into the database in a single query (which can be useful when dealing with large amounts of data that need to be inserted into the database). +* A new [`#average`](../../models-and-databases/reference/query-set.md#average) method was introduced to allow computing the average values of a specific model field at the database level for the records targeted by a specific query set. +* A new [`#sum`](../../models-and-databases/reference/query-set.md#sum) method was introduced to allow computing the sum of the values of a specific model field at the database level for the records targeted by a specific query set. +* It is now possible to compute the minimum and maximum values of a specific field at the database level for the records targeted by a query set through the use of the [`#minimum`](../../models-and-databases/reference/query-set.md#minimum) and [`#maximum`](../../models-and-databases/reference/query-set.md#maximum) methods. +* The [`in`](../../models-and-databases/reference/query-set.md#in) query set predicate now supports filtering on arrays of model records directly. +* Query sets now provide a [`#to_sql`](../../models-and-databases/reference/query-set.md#to_sql) method allowing to retrieve the corresponding SQL representation. +* Query sets can now be combined using the AND and OR binary operators. This can be achieved through the use of the [&](../../models-and-databases/reference/query-set.md#-and) and [`|`](../../models-and-databases/reference/query-set.md#-or) query set methods. +* Invalid record exceptions (instances of [`Marten::DB::Errors::InvalidRecord`](pathname:///api/0.5/Marten/DB/Errors/InvalidRecord.html)) now provide more details regarding the actual errors of the associated record. +* Creations of records from many-to-one reverse relation query sets are now scoped to the related record. See [Many-to-one relationships](../../models-and-databases/relationships.md#many-to-one-relationships) for more details. +* A new [`#build`](../../models-and-databases/reference/query-set.md#build) method was introduced to make it possible to initialize new model instances from query sets. + +#### Handlers and HTTP + +* The [`#render`](../../handlers-and-http/introduction.md#render) helper method and the generic handlers that involve template renderings now automatically insert the request object in the template context. +* The [`Marten::Handlers::RecordDetail`](../../handlers-and-http/reference/generic-handlers.md#displaying-a-record), [`Marten::Handlers::RecordUpdate`](../../handlers-and-http/reference/generic-handlers.md#updating-a-record), and [`Marten::Handlers::RecordDelete`](../../handlers-and-http/reference/generic-handlers.md#deleting-a-record) generic handlers now provide the ability to specify a custom [query set](../../models-and-databases/queries.md) instead of a model class. This can be achieved through the use of the [`#queryset`](pathname:///api/0.5/Marten/Handlers/RecordRetrieving.html#queryset(queryset)-macro) macro. +* A new middleware was introduced to make it easy to override the method of incoming requests based on the value of a specific request parameter or header: the [method override middleware](../../handlers-and-http/reference/middlewares.md#method-override-middleware). This mechanism is useful for overriding HTTP methods in HTML forms that natively support GET and POST methods only. A dedicated set of [settings](../../development/reference/settings.md#method-overriding-settings) is also available to easily customize the behavior of this middleware. +* The value of the max-age directive used for the Cache-Control header that is set by the [assets serving middleware](../../handlers-and-http/reference/middlewares.md#asset-serving-middleware) can now be configured in the [`assets.max_age`](../../development/reference/settings.md#max_age) setting. +* Subclasses of the [`Marten::Handlers::Template`](../../handlers-and-http/reference/generic-handlers.md#rendering-a-template) generic handler now support a [`#content_type`](pathname:///api/0.5/Marten/Handlers/Rendering/ClassMethods.html#content_type(content_type%3AString|Nil)-instance-method) class method that allows configuring the content type of the response (unless specified, the content type will default to `text/html`). + +#### Templates + +* Enum values can now be used in templates. Please refer to [Using enums in contexts](../../templates/introduction.md#using-enums-in-contexts) to learn more about this capability. +* Support for the `nil`, `true`, and `false` literals was added to the Marten templating language. Please refer to [Literal values](../../templates/introduction.md#literal-values) for more details on supported literals. +* The [`assign`](../../templates/reference/tags.md#assign) template tag now supports an `unless assigned` suffix which allows to specify that the assignment must happen only if no variable with the same name is already present in the template context. +* The [`include`](../../templates/reference/tags.md#include) template tag now supports `isolated` and `contextual` suffixes that allow specifying whether the included templates should have access to the outer context variables. Unless specified, the default behavior is controlled by the new [`templates.isolated_inclusions`](../../development/reference/settings.md#isolated_inclusions) setting. +* A new [`capture`](../../templates/reference/tags.md#capture) template tag was introduced to make it possible to easily assign the output of a block of code to a new variable. +* The output of the [`csrf_token`](../../templates/reference/tags.md#csrf_token) template tag can now be outputted to a variable by leveraging the `as` keyword. +* The `loop` variable, which is available when using the [`for`](../../templates/reference/tags.md#for) template tag, now supports `even?` and `odd?` attributes. +* A new [`csrf_input`](../../templates/reference/tags.md#csrf_input) template tag was introduced to make it easier to insert the CSRF token into HTML forms. This new tag allows the generation of a hidden form input containing the CSRF token computed for the current request. +* A new [`escape`](../../templates/reference/tags.md#escape) template tag was introduced to allow disabling/enabling [auto-escaping](../../templates/introduction.md#auto-escaping) within a block. +* A new [`unless`](../../templates/reference/tags.md#unless) template tag was introduced to provide an alternative way of writing conditional template blocks. + +#### Development + +* The [`collectassets`](../../development/reference/management-commands.md#collectassets) management command now provides the ability to fingerprint collected assets and generate a corresponding JSON manifest. Please refer to [Asset manifests and fingerprinting](../../assets/introduction.md#asset-manifests-and-fingerprinting) to learn more about this capability. +* The built-in [authentication application](../../authentication/introduction.md) (which can be generated either through the use of the [`new`](../../development/reference/management-commands.md#new) management command or the [`auth`](../../development/reference/generators.md#auth) generator) now uses POST requests when signing out users. +* A [`play`](../../development/reference/management-commands.md#play) management command was introduced to make it easy to start a Crystal playground server initialized for the current project and open it in the default browser. +* The [`new`](../../development/reference/management-commands.md#new) management command can now generate project/app structures that use the development version of Marten when the `--edge` (or `-e`) option is used. +* A new [`trailing_slash`](../../development/reference/settings.md#trailing_slash) setting was introduced to configure whether an HTTP permanent redirect (301) should be issued when an incoming URL that does not match any of the configured routes either ends with a slash or does not. +* A new `--open` option was added to the [`serve`](../../development/reference/management-commands.md#serve) management command to make it possible to automatically open the development server in the default browser. +* New projects created through the use of the [`new`](../../development/reference/management-commands.md#new) management command now have the debug log level enabled in development (see [`log_level`](../../development/reference/settings.md#log_level)). + +#### Security + +* It is now possible to configure that the [CSRF](../../security/csrf.md) token must be persisted in the session store. This can be achieved by setting the [`use_session`](../../development/reference/settings.md#use_session) setting to `true`. + +## Backward incompatible changes + +### Handlers and HTTP + +* Handlers making use of the [`Marten::Handlers::Schema`](pathname:///api/0.5/Marten/Handlers/Schema.html) generic handler (or schemas inheriting from it, such as [`Marten::Handlers::RecordCreate`](pathname:///api/0.5/Marten/Handlers/RecordCreate.html) or [`Marten::Handlers::RecordUpdate`](pathname:///api/0.5/Marten/Handlers/RecordUpdate.html)) will now return a **422 Unprocessable Content** response instead of a **200 OK** response when processing an invalid schema. While this won't have any incidence on end users, this change may make some specs fail if you were testing the response code of handlers that process invalid schema data. The way to fix this will be to replace `200` by `422` in the impacted specs. diff --git a/docs/versioned_sidebars/version-0.2-sidebars.json b/docs/versioned_sidebars/version-0.5-sidebars.json similarity index 76% rename from docs/versioned_sidebars/version-0.2-sidebars.json rename to docs/versioned_sidebars/version-0.5-sidebars.json index 5971fd602..dea48d98f 100644 --- a/docs/versioned_sidebars/version-0.2-sidebars.json +++ b/docs/versioned_sidebars/version-0.5-sidebars.json @@ -23,6 +23,7 @@ "items": [ "models-and-databases/introduction", "models-and-databases/queries", + "models-and-databases/relationships", "models-and-databases/validations", "models-and-databases/callbacks", "models-and-databases/migrations", @@ -41,6 +42,7 @@ "label": "Reference", "items": [ "models-and-databases/reference/fields", + "models-and-databases/reference/table-options", "models-and-databases/reference/query-set", "models-and-databases/reference/migration-operations" ] @@ -59,12 +61,15 @@ "handlers-and-http/routing", "handlers-and-http/generic-handlers", "handlers-and-http/error-handlers", - "handlers-and-http/middlewares", "handlers-and-http/sessions", + "handlers-and-http/cookies", + "handlers-and-http/callbacks", + "handlers-and-http/middlewares", { "type": "category", "label": "How-To's", "items": [ + "handlers-and-http/how-to/customize-handler-template-contexts", "handlers-and-http/how-to/create-custom-route-parameters" ] }, @@ -93,7 +98,8 @@ "items": [ "templates/how-to/create-custom-filters", "templates/how-to/create-custom-tags", - "templates/how-to/create-custom-context-producers" + "templates/how-to/create-custom-context-producers", + "templates/how-to/create-custom-loaders" ] }, { @@ -102,7 +108,8 @@ "items": [ "templates/reference/filters", "templates/reference/tags", - "templates/reference/context-producers" + "templates/reference/context-producers", + "templates/reference/loaders" ] } ] @@ -174,11 +181,13 @@ "development/settings", "development/applications", "development/management-commands", + "development/generators", "development/testing", { "type": "category", "label": "How-To's", "items": [ + "development/how-to/configure-database-backends", "development/how-to/create-custom-commands" ] }, @@ -187,7 +196,8 @@ "label": "Reference", "items": [ "development/reference/settings", - "development/reference/management-commands" + "development/reference/management-commands", + "development/reference/generators" ] } ] @@ -202,7 +212,8 @@ "items": [ "security/introduction", "security/csrf", - "security/clickjacking" + "security/clickjacking", + "security/content-security-policy" ] }, { @@ -225,6 +236,7 @@ }, "items": [ "emailing/introduction", + "emailing/callbacks", { "type": "category", "label": "How-To's", @@ -259,6 +271,31 @@ } ] }, + { + "type": "category", + "label": "Caching", + "link": { + "type": "doc", + "id": "caching" + }, + "items": [ + "caching/introduction", + { + "type": "category", + "label": "How-To's", + "items": [ + "caching/how-to/create-custom-cache-stores" + ] + }, + { + "type": "category", + "label": "Reference", + "items": [ + "caching/reference/stores" + ] + } + ] + }, { "type": "category", "label": "Deployment", @@ -272,7 +309,9 @@ "type": "category", "label": "How-To's", "items": [ - "deployment/how-to/deploy-to-an-ubuntu-server" + "deployment/how-to/deploy-to-an-ubuntu-server", + "deployment/how-to/deploy-to-heroku", + "deployment/how-to/deploy-to-fly-io" ] } ] @@ -286,6 +325,7 @@ }, "items": [ "the-marten-project/contributing", + "the-marten-project/design-philosophies", "the-marten-project/acknowledgments", { "type": "category", @@ -307,7 +347,19 @@ "the-marten-project/release-notes/0.2.1", "the-marten-project/release-notes/0.2.2", "the-marten-project/release-notes/0.2.3", - "the-marten-project/release-notes/0.2.4" + "the-marten-project/release-notes/0.2.4", + "the-marten-project/release-notes/0.3", + "the-marten-project/release-notes/0.3.1", + "the-marten-project/release-notes/0.3.2", + "the-marten-project/release-notes/0.3.3", + "the-marten-project/release-notes/0.3.4", + "the-marten-project/release-notes/0.4", + "the-marten-project/release-notes/0.4.1", + "the-marten-project/release-notes/0.4.2", + "the-marten-project/release-notes/0.4.3", + "the-marten-project/release-notes/0.4.4", + "the-marten-project/release-notes/0.4.5", + "the-marten-project/release-notes/0.5" ] } ] diff --git a/docs/versions.json b/docs/versions.json index 28959c222..1bd54b528 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,5 +1,5 @@ [ + "0.5", "0.4", - "0.3", - "0.2" + "0.3" ] diff --git a/shard.yml b/shard.yml index e5d5e02e7..0febdca8c 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: marten -version: 0.4.5 +version: 0.5.0 authors: - Morgan Aubert diff --git a/src/marten.cr b/src/marten.cr index 51b9a9894..2134eef62 100644 --- a/src/marten.cr +++ b/src/marten.cr @@ -40,7 +40,7 @@ require "./marten/server/**" require "./marten/template/**" module Marten - VERSION = "0.4.5" + VERSION = "0.5.0" Log = ::Log.for("marten")