diff --git a/Makefile b/Makefile index 50df5e755..755394476 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.1 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 .PHONY: docs_site docs_site: 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 738c70c92..bf79ecfbc 100644 --- a/docs/docs/the-marten-project/release-notes/0.4.md +++ b/docs/docs/the-marten-project/release-notes/0.4.md @@ -61,7 +61,7 @@ You can read more about this new kind of model inheritance in [Multi table inher ### Schema handler callbacks -Handlers that inherit from the base schema handler - [`Marten::Handlers::Schema`](pathname:///api/dev/Marten/Handlers/Schema.html) - or one of its subclasses (such as [`Marten::Handlers::RecordCreate`](pathname:///api/dev/Marten/Handlers/RecordCreate.html) or [`Marten::Handlers::RecordUpdate`](pathname:///api/dev/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): +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) @@ -116,25 +116,25 @@ end #### 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/dev/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/dev/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/dev/Marten/DB/Query/ManyToManySet.html#add(*objs%3AM)-instance-method) and [`#remove`](pathname:///api/dev/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. +* 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/dev/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. +* 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/dev/Marten/Handlers/Schema.html) generic handler now allows modifying the schema object context name through the use of the [`#schema_context_name`](pathname:///api/dev/Marten/Handlers/Schema.html#schema_context_name(name%3AString|Symbol)-class-method) method. +* 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/dev/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`](#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 @@ -147,8 +147,8 @@ end * 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/dev/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/dev/Marten/CLI/Manage/Command/Base.html#on_invalid_option(%26block%3AString->)-instance-method) method. +* 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 @@ -162,7 +162,7 @@ end ### Handlers and HTTP -* Custom [session stores](../../handlers-and-http/sessions.md#session-stores) must now implement a [`#clear_expired_entries`](pathname:///api/dev/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). +* 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 diff --git a/docs/versioned_docs/version-0.1/development/testing.md b/docs/versioned_docs/version-0.1/development/testing.md deleted file mode 100644 index 99ae3b6ee..000000000 --- a/docs/versioned_docs/version-0.1/development/testing.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Testing -description: Learn how to test your Marten project. -sidebar_label: Testing ---- - -This section covers the basics regarding how to test a Marten project and the various tools that you can leverage in this regard. - -## The basics - -You should test your Marten project to ensure that it adheres to the specifications it was built for. Like any Crystal project, Marten lets you write "specs" (see the [official documentation related to testing in Crystal](https://crystal-lang.org/reference/guides/testing.html) to learn more about those). - -By default, when creating a project through the use of the [`new`](./reference/management-commands.md#new) management command, Marten will automatically create a `spec/` folder at the root of your project structure. This folder contains a unique `spec_helper.cr` file allowing you to initialize the test environment for your Marten project. - -This file should look something like this: - -```crystal title=spec/spec_helper.cr -ENV["MARTEN_ENV"] = "test" - -require "spec" -require "marten" -require "marten/spec" - -require "../src/project" -``` - -As you can see, the `spec_helper.cr` file forces the Marten environment variable to be set to `test` and requires the spec library as well as Marten and your actual project. This file should be required by all your spec files. - -:::info -It's very important to require `marten/spec` in your top-level spec helper as this will ensure that the mandatory spec callbacks are configured for your spec suite (eg. in order to ensure that your database is properly set up before each spec is executed). -::: - -When it comes to running your tests, you can simply make use of the standard [`crystal spec`](https://crystal-lang.org/reference/man/crystal/index.html#crystal-spec) command. - -## Writing tests - -To write tests, you should write regular [specs](https://crystal-lang.org/reference/guides/testing.html) and ensure that your spec files always require the `spec/spec_helper.cr` file. - -For example: - -```crystal -require "./spec_helper" - -describe MySuperAbstraction do - describe "#foo" do - it "returns bar" do - obj = MySuperAbstraction.new - obj.foo.should eq "bar" - end - end -end -``` - -You are encouraged to organize your spec files by following the structure of your projects. For example, you could create a `models` folder and define specs related to your models in it. - -:::tip -When organizing spec files across multiple folders, one good practice is to define a `spec_helper.cr` file at each level of your folders structure. These additional `spec_helper.cr` files should require the same file from the parent folder. - -For example: - -```crystal title=spec/models/spec_helper.cr -require "../spec_helper" -``` - -```crystal title=spec/models/article_spec.cr -require "./spec_helper" - -describe Article do - # ... -end -``` -::: - -## Running tests - -As mentioned before, running specs involves making use of the standard [`crystal spec`](https://crystal-lang.org/reference/man/crystal/index.html#crystal-spec) command. - -### The test environment - -By default, the [`new`](./reference/management-commands.md#new) management command always creates a `test` environment when generating new projects. As such, you should ensure that the `MARTEN_ENV` environment variable is set to `test` when running your Crystal specs. It should also be reminded that this `test` environment is associated with a dedicated settings file where test-related settings can be specified and/or overridden if necessary (see [Settings](./settings.md#environments) for more details about this). - -### The test database - -Marten **must** use a different database when running tests in order to not tamper with your regular database. Indeed, the database used in the context of specs will be flushed and generated automatically every time the specs suite is executed. You should not set these database names to the same names as the ones used for your development or production environments. If test database names are not explicitly set, your specs suite won't be allowed to run at all. - -One way to ensure you use a dedicated database specifically for tests is to override the [`database`](./reference/settings.md#database-settings) settings as follows: - -```crystal title=config/settings/test.cr -Marten.configure :test do |config| - config.database do |db| - db.name = "my_project_test" - end -end -``` diff --git a/docs/versioned_docs/version-0.1/schemas/reference/fields.md b/docs/versioned_docs/version-0.1/schemas/reference/fields.md deleted file mode 100644 index 5cb50cf54..000000000 --- a/docs/versioned_docs/version-0.1/schemas/reference/fields.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Schema fields -description: Schema fields reference. ---- - -This page provides a reference for all the available field options and field types that can be used when defining schemas. - -## Common field options - -The following field options can be used for all the available field types when declaring schema fields using the `field` macro. - -### `required` - -The `required` argument can be used to specify whether a schema field is required or not required. The default value for this argument is `true`. - -## Field types - -### `bool` - -A `bool` field allows validating boolean values. - -### `date_time` - -A `date_time` field allows validating date time values. Fields using this type are converted to `Time` objects in Crystal. - -### `date` - -A `date` field allows validating date values. Fields using this type are converted to `Time` objects in Crystal. - -### `file` - -A `file` field allows validating uploaded files. In addition to the [common field options](#common-field-options), such fields support the following arguments: - -#### `allow_empty_files` - -The `allow_empty_files` argument allows defining whether empty files are allowed or not when files are validated. The default value is `false`. - -#### `max_name_size` - -The `max_name_size` argument allows defining the maximum file name size allowed. The default value is `nil`, which means that uploaded file name sizes are not validated. - -### `float` - -A `float` field allows validating float values. Fields using this type are converted to `Float64` objects in Crystal. In addition to the [common field options](#common-field-options), such fields support the following arguments: - -#### `max_value` - -The `max_value` argument allows defining the maximum value allowed. The default value for this argument is `nil`, which means that the maximum value is not validated by default. - -#### `min_value` - -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. - -### `int` - -An `int` field allows validating integer values. Fields using this type are converted to `Int64` objects in Crystal. In addition to the [common field options](#common-field-options), such fields support the following arguments: - -#### `max_value` - -The `max_value` argument allows defining the maximum value allowed. The default value for this argument is `nil`, which means that the maximum value is not validated by default. - -#### `min_value` - -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. - -### `string` - -A `string` field allows validating string 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 string. The default value for this argument is `nil`, which means that the maximum size is not validated by default. - -#### `min_size` - -The `min_size` argument allows defining the minimum size allowed for the 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`. - -### `uuid` - -A `uuid` field allows validating Universally Unique IDentifiers (UUID) values. Fields using this type are converted to `UUID` objects in Crystal. diff --git a/docs/versioned_docs/version-0.1/static/img/getting-started/tutorial/marten_welcome_page.png b/docs/versioned_docs/version-0.1/static/img/getting-started/tutorial/marten_welcome_page.png deleted file mode 100644 index 558c1e862..000000000 Binary files a/docs/versioned_docs/version-0.1/static/img/getting-started/tutorial/marten_welcome_page.png and /dev/null differ diff --git a/docs/versioned_docs/version-0.1/templates/reference/filters.md b/docs/versioned_docs/version-0.1/templates/reference/filters.md deleted file mode 100644 index 1fafed4c0..000000000 --- a/docs/versioned_docs/version-0.1/templates/reference/filters.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Template filters -description: Template filters reference. ---- - -This page provides a reference for all the available filters that can be used when defining [templates](../introduction.md). - -## `capitalize` - -The `capitalize` filter allows to modify a string so that the first letter is converted to uppercase and all the subsequent letters are converted to lowercase. - -For example: - -```html -{{ value|capitalize }} -``` - -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. - -For example: - -```html -{{ value|default:"foobar" }} -``` - -If `value` is `nil` (or `0` or `false`), the output will be "foobar". - -## `downcase` - -The `downcase` filter allows to convert a string so that each of its characters is lowercase. - -For example: - -```html -{{ value|downcase }} -``` - -If `value` is "Hello", then the output will be "hello". - -## `safe` - -The `safe` filter allows to mark that a string is safe and that it should not be escaped before being inserted in the final output of a rendered template. Indeed, string values are always automatically HTML-escaped by default in templates. - -For example: - -```html -{{ value|safe }} -``` - -If `value` is `

Hello

`, then the output will be `

Hello

` as well. - -## `size` - -The `size` filter allows returning the size of a string or an enumerable object. - -For example: - -```html -{{ value|size }} -``` - -If `value` is `hello`, then the output will be 5. - -## `upcase` - -The `upcase` filter allows to convert a string so that each of its characters is uppercase. - -For example: - -```html -{{ value|upcase }} -``` - -If `value` is "Hello", then the output will be "HELLO". diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes.md b/docs/versioned_docs/version-0.1/the-marten-project/release-notes.md deleted file mode 100644 index f10f20aa6..000000000 --- a/docs/versioned_docs/version-0.1/the-marten-project/release-notes.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Release notes -description: Find all the release notes of the Marten web framework. -pagination_next: null ---- - -Here are listed the release notes for each version of the Marten web framework. - -## Marten 0.1 - -* [Marten 0.1.5 release notes](./release-notes/0.1.5.md) -* [Marten 0.1.4 release notes](./release-notes/0.1.4.md) -* [Marten 0.1.3 release notes](./release-notes/0.1.3.md) -* [Marten 0.1.2 release notes](./release-notes/0.1.2.md) -* [Marten 0.1.1 release notes](./release-notes/0.1.1.md) -* [Marten 0.1 release notes](./release-notes/0.1.md) diff --git a/docs/versioned_docs/version-0.4/assets.mdx b/docs/versioned_docs/version-0.4/assets.mdx new file mode 100644 index 000000000..f0eabd530 --- /dev/null +++ b/docs/versioned_docs/version-0.4/assets.mdx @@ -0,0 +1,15 @@ +--- +title: Assets +--- + +import DocCard from '@theme/DocCard'; + +Marten provides a set of helpers in order to help you manage and resolve assets (also known as "static files") such as images, Javascript files, CSS files, etc. + +## Guides + +
+
+ +
+
diff --git a/docs/versioned_docs/version-0.1/files/asset-handling.md b/docs/versioned_docs/version-0.4/assets/introduction.md similarity index 84% rename from docs/versioned_docs/version-0.1/files/asset-handling.md rename to docs/versioned_docs/version-0.4/assets/introduction.md index 375744698..03e1f933e 100644 --- a/docs/versioned_docs/version-0.1/files/asset-handling.md +++ b/docs/versioned_docs/version-0.4/assets/introduction.md @@ -1,7 +1,7 @@ --- -title: Asset handling +title: Assets handling description: Learn how to handle assets. -sidebar_label: Asset handling +sidebar_label: Introduction --- Web applications generally need to serve "static files" or "assets": static images, Javascript files, CSS files, etc. Marten provides a set of helpers in order to help you manage assets, refer to them, and upload them to specific storages. @@ -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.1/Marten/Asset/Engine.html#url(filepath%3AString)%3AString-instance-method) in Crystal +* by using the [assets engine](pathname:///api/0.4/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.1/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.4/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.1/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.4/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 @@ -78,7 +78,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.1/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.4/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 +86,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.1/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.4/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 +101,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.1/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.4/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 +118,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.1/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.4/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 +157,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.1/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.4/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.4/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.4/authentication.mdx b/docs/versioned_docs/version-0.4/authentication.mdx new file mode 100644 index 000000000..882cc8731 --- /dev/null +++ b/docs/versioned_docs/version-0.4/authentication.mdx @@ -0,0 +1,23 @@ +--- +title: Authentication +--- + +import DocCard from '@theme/DocCard'; + +Marten provides the ability to generate projects with a built-in authentication system that handles basic user management needs. This section gives detail on how this authentication system works out of the box, and how you can extend it to suit your project's needs. + +## Guides + +
+
+ +
+
+ +## Reference + +
+
+ +
+
diff --git a/docs/versioned_docs/version-0.4/authentication/introduction.md b/docs/versioned_docs/version-0.4/authentication/introduction.md new file mode 100644 index 000000000..6859b28e4 --- /dev/null +++ b/docs/versioned_docs/version-0.4/authentication/introduction.md @@ -0,0 +1,239 @@ +--- +title: Introduction to authentication +description: Learn how to set up authentication for your Marten project. +sidebar_label: Introduction +--- + +Marten allows the generation of new projects with a built-in authentication application that handles basic user management needs. You can then extend and adapt this application so that it accommodates your project's needs. + +## Overview + +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: + +* Signing up users +* Signing in users +* Signing out users +* Allowing users to reset their passwords +* Allowing users to access a basic profile page + +Internally, this authentication application relies on the official [`marten-auth`](https://github.com/martenframework/marten-auth) shard. This shard implements low-level authentication operations (such as authenticating user credentials, generating securely encrypted passwords, generating password reset tokens, etc). + +## Generating projects with authentication + +Generating new projects with authentication can be easily achieved by leveraging the `--with-auth` option of the [`new`](../development/reference/management-commands.md#new) management command. + +For example: + +```bash +marten new project myblog --with-auth +``` + +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`). + +:::info +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. + +### The `User` model + +The `auth` application defines a single `Auth::User` model that inherits its fields from the abstract `MartenAuth::User` model. As such, this model automatically provides the following fields: + +* `id` - a [`big_int`](../models-and-databases/reference/fields.md#big_int) field containing the primary key of the user +* `email` - an [`email`](../models-and-databases/reference/fields.md#email) field containing the user's email address +* `password` - a [`string`](../models-and-databases/reference/fields.md#string) field containing the user's encrypted password +* `created_at` - a [`date_time`](../models-and-databases/reference/fields.md#date_time) field containing the user creation date +* `updated_at` - a [`date_time`](../models-and-databases/reference/fields.md#date_time) field containing the last user modification date + +### 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.4/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.4/Marten/HTTP/Request.html) object in order to interact with the currently signed-in user: + +| Method | Description | +| --- | --- | +| `#user_id` | Returns the current user ID associated with the considered request, or `nil` if there is no authenticated user. | +| `#user` | Returns the user associated with the request, or `nil` if there is no authenticated user. | +| `#user!` | Returns the user associated with the request, or raise `NilAssertionError` if there is no authenticated user. | +| `#user?` | Returns `true` if a user is authenticated for the request. | + +This makes it possible to easily check whether a user is authenticated in handlers in order to implement different logic. For example: + +```crystal +class MyHandler < Marten::Handler + def get + if request.user? + respond "User ##{request.user!.id} is signed-in" + else + respond "No signed-in user" + end + end +end +``` + +### Creating users + +Creating a user is as simple as initializing an instance of the `Auth::User` model and defining its properties. That being said it is important to note that the user's password (`password` field) must be set using the `#set_password` method: this will ensure that the _raw_ password you provide to this method is properly encrypted and that the resulting hash is assigned to the `password` field. Because of this, you should not attempt to manipulate the `password` field attribute of user records directly. + +For example: + +```crystal +user = Auth::User.new(email: "test@example.com") do |user| + user.set_password("insecure") +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. + +For example: + +```crystal +user = MartenAuth.authenticate("test@example.com", "insecure") +if user + puts "User credentials are valid!" +else + puts "User credentials are not valid!" +end +``` + +:::caution +It is important to realize that this method _only_ verifies user credentials. **It does not sign in users** for a specific request. Signing in users (and attaching them to the current session) is handled by the `#sign_in` method, which is discussed in [Signing in users](#signing-in-users). +::: + +:::info +The `MartenAuth#authenticate` method is automatically used by the handlers that are generated for your `auth` application before signing in users. +::: + +### 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.4/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: + +```crystal +class MyHandler < Marten::Handler + def post + user = MartenAuth.authenticate(request.data["email"].to_s, request.data["password"].to_s) + + if user + MartenAuth.sign_in(request, user) + redirect reverse("auth:profile") + else + redirect reverse("auth:sign_in") + end + end +end +``` + +:::caution +It is important to understand that this method is intended to be used for a user record whose credentials were validated using the `#authenticate` method beforehand. See [Authenticating users](#authenticating-users) for more details. +::: + +### 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.4/Marten/HTTP/Request.html)) as argument, removes the authenticated user ID from the current request, and flushes the associated session data. + +For example: + +```crystal +class MyHandler < Marten::Handler + def get + MartenAuth.sign_out(request) + redirect reverse("auth:sign_in") + end +end +``` + +### Changing a user's password + +The ability to change a user password is provided by the `#set_password` method of the `Auth::User` model (which is inherited from the `MartenAuth::User` abstract class that is provided by the [`marten-auth`](https://github.com/martenframework/marten-auth) shard). + +For example: + +```crystal +use = User.get!(email: "test@example.com") +user.set_password("insecure") +user.save! +``` + +:::info +Passwords are encrypted using [`Crypto::Bcrypt`](https://crystal-lang.org/api/Crypto/Bcrypt.html). +::: + +As mentioned previously, you should not attempt to manipulate the `password` field directly: this field contains the hash value that results from the encryption of the raw password. + +### 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.4/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: + +```crystal +class UserProfileHandler < Marten::Handler + before_dispatch :require_signed_in_user + + def get + render "auth/profile.html" { user: request.user } + end + + private def require_signed_in_user + redirect reverse("auth:sign_in") unless request.user? + end +end +``` + +It should be noted that the `auth` application generated for your project already contains an `Auth::RequireSignedInUser` concern module that you can include in your handlers in order to ensure that they can only be accessed by signed-in users (and that anonymous users are redirected to the sign-in page). + +For example: + +```crystal +class UserProfileHandler < Marten::Handler + include Auth::RequireSignedInUser + + def get + render "auth/profile.html" { user: request.user } + end +end +``` diff --git a/docs/versioned_docs/version-0.4/authentication/reference/generated-files.md b/docs/versioned_docs/version-0.4/authentication/reference/generated-files.md new file mode 100644 index 000000000..284a800db --- /dev/null +++ b/docs/versioned_docs/version-0.4/authentication/reference/generated-files.md @@ -0,0 +1,60 @@ +--- +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 or when using the [`auth`](../../development/reference/generators.md#auth) generator. + +## Application + +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 +* `routes.cr` - The `auth` application routes map + +### Emails + +* `password_reset_email.cr` - Defines the [email](../../emailing.mdx) that is sent as part of the user password reset flow + +### Handlers + +* `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 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 +* `handlers/sign_up_handler.cr` - A handler that allows users to sign up + +### Migrations + +* `migrations/0001_create_auth_user_table.cr` - Allows to create the table of the `Auth::User` model + +### Models + +* `models/user.cr` - Defines the main `Auth::User` model + +### Schemas + +* `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 + +### Templates + +* `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/apps/auth` folder. diff --git a/docs/versioned_docs/version-0.4/caching.mdx b/docs/versioned_docs/version-0.4/caching.mdx new file mode 100644 index 000000000..d0e37df10 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/caching/how-to/create-custom-cache-stores.md b/docs/versioned_docs/version-0.4/caching/how-to/create-custom-cache-stores.md new file mode 100644 index 000000000..aa48a15ec --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/Marten/Cache/Store/Base.html) abstract class and that implements the following methods: + +* [`#clear`](pathname:///api/0.4/Marten/Cache/Store/Base.html#clear-instance-method) - called when clearing the cache +* [`#decrement`](pathname:///api/0.4/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.4/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.4/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.4/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-store1) setting. + +For example: + +```crystal +config.cache_store = MemoryStore.new +``` diff --git a/docs/versioned_docs/version-0.4/caching/introduction.md b/docs/versioned_docs/version-0.4/caching/introduction.md new file mode 100644 index 000000000..791aeeeaa --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.new(expires_in: 24.hours) +end +``` + +:::info +By default, Marten uses an in-memory cache (instance of [`Marten::Cache::Store::Memory`](pathname:///api/0.4/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.4/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.4/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.4/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.4/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.4/Marten/Cache/Store/Base.html#read(key%3AString|Symbol%2Cversion%3AInt32|Nil%3Dnil)%3AString|Nil-instance-method) and [`#write`](pathname:///api/0.4/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.4/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.4/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.4/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.4/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.4/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.4/caching/reference/stores.md b/docs/versioned_docs/version-0.4/caching/reference/stores.md new file mode 100644 index 000000000..22f3dcf75 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.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.4/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.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.1/deployment.mdx b/docs/versioned_docs/version-0.4/deployment.mdx similarity index 64% rename from docs/versioned_docs/version-0.1/deployment.mdx rename to docs/versioned_docs/version-0.4/deployment.mdx index 1e884b97d..f6ecb29b6 100644 --- a/docs/versioned_docs/version-0.1/deployment.mdx +++ b/docs/versioned_docs/version-0.4/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.1/deployment/how-to/deploy-to-an-ubuntu-server.md b/docs/versioned_docs/version-0.4/deployment/how-to/deploy-to-an-ubuntu-server.md similarity index 94% rename from docs/versioned_docs/version-0.1/deployment/how-to/deploy-to-an-ubuntu-server.md rename to docs/versioned_docs/version-0.4/deployment/how-to/deploy-to-an-ubuntu-server.md index 6758f6504..4428fec6d 100644 --- a/docs/versioned_docs/version-0.1/deployment/how-to/deploy-to-an-ubuntu-server.md +++ b/docs/versioned_docs/version-0.4/deployment/how-to/deploy-to-an-ubuntu-server.md @@ -28,7 +28,7 @@ Alternatively, you can also refer to [Crystal's official installation instructio Secondly, we should install a few additional packages that will be required later on: * `git` to clone the project's repository -* `nginx` to serve the project's server behind a reverse proxy and also serve [assets](../../files/asset-handling) and [media files](../../files/managing-files.md) +* `nginx` to serve the project's server behind a reverse proxy and also serve [assets](../../assets/introduction.md) and [media files](../../files/managing-files.md) * `postgresql` to handle our database needs This can be achieved by running the following command: @@ -111,13 +111,13 @@ Depending on how you are handling assets as part of your projects you may have t ## Collect assets -You will then want to collect your [assets](../../files/asset-handling) so that they are uploaded to their final destination. To do so you can leverage the management CLI binary you compiled previously and run the [`collectassets`](../../development/reference/management-commands.md#collectassets) command: +You will then want to collect your [assets](../../assets/introduction.md) so that they are uploaded to their final destination. To do so you can leverage the management CLI binary you compiled previously and run the [`collectassets`](../../development/reference/management-commands.md#collectassets) command: ```bash bin/manage collectassets --no-input ``` -This management command will "collect" all the available assets from the applications' assets directories and from the directories configured in the [`dirs`](../../development/reference/settings.md#dirs) setting, and ensure that they are "uploaded" to their final destination based on the [assets storage](../../files/asset-handling#assets-storage) that is currently configured. +This management command will "collect" all the available assets from the applications' assets directories and from the directories configured in the [`dirs`](../../development/reference/settings.md#dirs) setting, and ensure that they are "uploaded" to their final destination based on the [assets storage](../../assets/introduction.md#assets-storage) that is currently configured. ## Apply the project's migrations diff --git a/docs/versioned_docs/version-0.4/deployment/how-to/deploy-to-fly-io.md b/docs/versioned_docs/version-0.4/deployment/how-to/deploy-to-fly-io.md new file mode 100644 index 000000000..cdbc85a0c --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.4/deployment/how-to/deploy-to-heroku.md b/docs/versioned_docs/version-0.4/deployment/how-to/deploy-to-heroku.md new file mode 100644 index 000000000..9ffdfbd2a --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.1/deployment/introduction.md b/docs/versioned_docs/version-0.4/deployment/introduction.md similarity index 95% rename from docs/versioned_docs/version-0.1/deployment/introduction.md rename to docs/versioned_docs/version-0.4/deployment/introduction.md index b7a4f31da..1e63d4eb6 100644 --- a/docs/versioned_docs/version-0.1/deployment/introduction.md +++ b/docs/versioned_docs/version-0.4/deployment/introduction.md @@ -12,7 +12,7 @@ Each deployment pipeline is unique and will vary from one project to another. Th 1. installing your project's dependencies 2. compiling your project's server and [management CLI](../development/management-commands.md) -3. collecting your project's [assets](../files/asset-handling) +3. collecting your project's [assets](../assets/introduction.md) 4. applying any pending migrations to your database 5. starting the compiled server @@ -61,7 +61,7 @@ bin/manage collectassets --no-input ``` :::info -The assets handling documentation also provides a few [guidelines](../files/asset-handling#serving-assets-in-production) on how to serve asset files in production that may be worth reading. +The assets handling documentation also provides a few [guidelines](../assets/introduction.md#serving-assets-in-production) on how to serve asset files in production that may be worth reading. ::: ### Applying migrations @@ -86,7 +86,7 @@ bin/server It's important to note that the Marten server is intended to be used behind a reverse proxy such as [Nginx](https://www.nginx.com/) or [Apache](https://httpd.apache.org/): you will usually want to configure such reverse proxy so that it targets your configured Marten server host and port. In this light, you should ensure that your Marten server is not using the HTTP port 80 (instead it could use something like 8080 or 8000 for example). -Depending on your use cases, a reverse proxy will also allow you to easily serve other contents such as [assets](../files/asset-handling) or [uploaded files](../files/managing-files.md), and to use SSL/TLS. +Depending on your use cases, a reverse proxy will also allow you to easily serve other contents such as [assets](../assets/introduction.md) or [uploaded files](../files/managing-files.md), and to use SSL/TLS. :::tip It is possible to run multiple processes of the same server behind a reverse proxy such as Nginx. Indeed, each compiled server can accept optional parameters to override the host and/or port being used. These parameters are respectively `--bind` (or `-b`) and `--port` (or `-p`). For example: diff --git a/docs/versioned_docs/version-0.1/development.mdx b/docs/versioned_docs/version-0.4/development.mdx similarity index 73% rename from docs/versioned_docs/version-0.1/development.mdx rename to docs/versioned_docs/version-0.4/development.mdx index e8135349c..dc566b2e8 100644 --- a/docs/versioned_docs/version-0.1/development.mdx +++ b/docs/versioned_docs/version-0.4/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.1/development/applications.md b/docs/versioned_docs/version-0.4/development/applications.md similarity index 75% rename from docs/versioned_docs/version-0.1/development/applications.md rename to docs/versioned_docs/version-0.4/development/applications.md index 389ff5293..73e4dba9e 100644 --- a/docs/versioned_docs/version-0.1/development/applications.md +++ b/docs/versioned_docs/version-0.4/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. @@ -18,7 +18,7 @@ Another benefit of applications is that they can be packaged and reused across m 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. -This setting corresponds to an array of installed app classes. Indeed, each Marten application must define a subclass of [`Marten::App`](pathname:///api/0.1/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.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. For example: @@ -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 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.1/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.1/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.4/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.4/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 diff --git a/docs/versioned_docs/version-0.4/development/generators.md b/docs/versioned_docs/version-0.4/development/generators.md new file mode 100644 index 000000000..54c1b6457 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/development/how-to/configure-database-backends.md b/docs/versioned_docs/version-0.4/development/how-to/configure-database-backends.md new file mode 100644 index 000000000..e6dd701aa --- /dev/null +++ b/docs/versioned_docs/version-0.4/development/how-to/configure-database-backends.md @@ -0,0 +1,155 @@ +--- +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 **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 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) +* [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 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: + +### 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: + +### 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. + +### 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.1/development/how-to/create-custom-commands.md b/docs/versioned_docs/version-0.4/development/how-to/create-custom-commands.md similarity index 85% rename from docs/versioned_docs/version-0.1/development/how-to/create-custom-commands.md rename to docs/versioned_docs/version-0.4/development/how-to/create-custom-commands.md index 61f5538dd..3282f6784 100644 --- a/docs/versioned_docs/version-0.1/development/how-to/create-custom-commands.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.1/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.4/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.1/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.4/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.4/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.1/development/management-commands.md b/docs/versioned_docs/version-0.4/development/management-commands.md similarity index 78% rename from docs/versioned_docs/version-0.1/development/management-commands.md rename to docs/versioned_docs/version-0.4/development/management-commands.md index 3dc71ba07..35657d0f4 100644 --- a/docs/versioned_docs/version-0.1/development/management-commands.md +++ b/docs/versioned_docs/version-0.4/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.4/development/reference/generators.md b/docs/versioned_docs/version-0.4/development/reference/generators.md new file mode 100644 index 000000000..e47959df8 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.1/development/reference/management-commands.md b/docs/versioned_docs/version-0.4/development/reference/management-commands.md similarity index 58% rename from docs/versioned_docs/version-0.1/development/reference/management-commands.md rename to docs/versioned_docs/version-0.4/development/reference/management-commands.md index 653105272..28ac6dd13 100644 --- a/docs/versioned_docs/version-0.1/development/reference/management-commands.md +++ b/docs/versioned_docs/version-0.4/development/reference/management-commands.md @@ -6,13 +6,32 @@ 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]` Collects all the assets and copies them into a unique storage. -Please refer to [Asset handling](../../files/asset-handling) to learn more about when and how assets are "collected". +Please refer to [Asset handling](../../assets/introduction.md) to learn more about when and how assets are "collected". ### Options @@ -25,6 +44,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 +135,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 @@ -100,26 +153,35 @@ marten migrate foo 202203111821451 # Applies (or unapply) migrations for the "fo ## `new` -**Usage:** `marten new [options] [type] [name] [dir]` +**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. +### Options + +* `-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. + ### Arguments * `type` - The type of structure to create (must be either `project` or `app`) * `name` - The name of the project or app to create -* `dir` - A destination directory (optional) + +:::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 ./projects/myblog # Creates a "myblog" project in the "./projects/myblog" folder -marten new app auth # Creates an "auth" application +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 repository structure ``` ## `resetmigrations` @@ -150,6 +212,26 @@ Displays all the routes of the application. Starts a development server that is automatically recompiled when source files change. +### Options + +* `-b HOST, --bind=HOST` - Allows specifying a custom host to bind +* `-p PORT, --port=PORT` - Allows specifying a custom port to listen for connections + +### 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 +``` + +:::tip +You can also use the alias `s` to start the development server: + +```bash +marten s +``` +::: + ## `version` **Usage:** `marten version [options]` diff --git a/docs/versioned_docs/version-0.1/development/reference/settings.md b/docs/versioned_docs/version-0.4/development/reference/settings.md similarity index 68% rename from docs/versioned_docs/version-0.1/development/reference/settings.md rename to docs/versioned_docs/version-0.4/development/reference/settings.md index 50e5d35a9..0bd9115a6 100644 --- a/docs/versioned_docs/version-0.1/development/reference/settings.md +++ b/docs/versioned_docs/version-0.4/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.4/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.4/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.4/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.1/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.4/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` @@ -92,6 +102,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: `""` @@ -160,7 +184,7 @@ The value to use for the X-Frame-Options header when the associated middleware i ## Assets settings -Assets settings allow configuring how Marten should interact with [assets](../../files/asset-handling). These settings are all available under the `assets` namespace: +Assets settings allow configuring how Marten should interact with [assets](../../assets/introduction.md). These settings are all available under the `assets` namespace: ```crystal config.assets.root = "assets" @@ -171,7 +195,7 @@ config.assets.url = "/assets/" Default: `true` -A boolean indicating whether assets should be looked for inside installed application folders. When this setting is set to `true`, this means that assets provided by installed applications will be collected by the `collectassets` command (please refer to [Asset handling](../../files/asset-handling) for more details regarding how to manage assets in your project). +A boolean indicating whether assets should be looked for inside installed application folders. When this setting is set to `true`, this means that assets provided by installed applications will be collected by the `collectassets` command (please refer to [Asset handling](../../assets/introduction.md) for more details regarding how to manage assets in your project). ### `dirs` @@ -208,9 +232,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.1/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.4/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.1/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.4/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 +242,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.1/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.4/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`. @@ -294,6 +318,52 @@ config.csrf.trusted_origins = [ ] ``` +## 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.4/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.4/Marten/HTTP/ContentSecurityPolicy.html) object through the use of methods such as [`#default_src=`](pathname:///api/0.4/Marten/HTTP/ContentSecurityPolicy.html#default_src%3D(value%3AArray|Nil|String|Symbol|Tuple)-instance-method) or [`#frame_src=`](pathname:///api/0.4/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. @@ -336,18 +406,62 @@ The database backend to use for connecting to the considered database. Marten su * `:postgresql` * `:sqlite` +### `checkout_timeout` + +Default: `5.0` + +The number of seconds to wait for a connection to become available when the max pool size is reached. + ### `host` Default: `nil` A string containing the host used to connect to the database. No value means that the host will be localhost. +### `initial_pool_size` + +Default: `1` + +The initial number of connections created for the database connections pool. + +### `max_idle_pool_size` + +Default: `1` + +The maximum number of idle connections for the database connections pool. Concretely, this means that when released, a connection will be closed only if there are already `max_idle_pool_size` idle connections. + +### `max_pool_size` + +Default: `0` + +The maximum number of connections that will be held by the database connections pool. When set to `0`, this means that there is no limit to the number of connections. + ### `name` 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` @@ -360,12 +474,51 @@ Default: `nil` The port to use to connect to the configured database. No value means that the default port will be used. +### `retry_attempts` + +Default: `1` + +The maximum number of attempts to retry re-establishing a lost connection. + +### `retry_delay` + +Default: `1.0` + +The delay to wait between each retry at re-establishing a lost connection. + ### `user` Default: `nil` A string containing the name of the user that should be used to connect to the configured database. +## Emailing settings + +Emailing settings allow configuring emailing-related settings. Please refer to [Emailing](../../emailing.mdx) for more details about how to define and send emails in your projects. + +The following settings are all available under the `emailing` namespace: + +```crystal +config.emailing.from_address = "no-reply@example.com" +config.emailing.backend = Marten::Emailing::Backend::Development.new(print_emails: true) +``` + +### `backend` + +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.4/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). + +### `from_address` + +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.4/Marten/Emailing/Address.html) object (which allows to specify the name AND the address of the sender email). + ## I18n settings I18n settings allow configuring internationalization-related settings. Please refer to [Internationalization](../../i18n.mdx) for more details about how to leverage translations and localized content in your projects. @@ -396,6 +549,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: @@ -419,9 +578,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.1/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.4/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.1/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.4/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. @@ -429,7 +588,7 @@ 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.1/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.4/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`. @@ -489,6 +648,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: @@ -565,3 +745,9 @@ config.templates.dirs = [ :"src/path2/templates", ] ``` + +### `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.4/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.1/development/settings.md b/docs/versioned_docs/version-0.4/development/settings.md similarity index 88% rename from docs/versioned_docs/version-0.1/development/settings.md rename to docs/versioned_docs/version-0.4/development/settings.md index 8cd7fbcbb..21ec5a402 100644 --- a/docs/versioned_docs/version-0.1/development/settings.md +++ b/docs/versioned_docs/version-0.4/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.1/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. This method returns a [`Marten::Conf::GlobalSettings`](pathname:///api/0.1/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.4/Marten.html#configure(env%3ANil|String|Symbol%3Dnil%2C%26)-class-method) method. This method returns a [`Marten::Conf::GlobalSettings`](pathname:///api/0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/Marten.html#env-class-method) method, which returns a [`Marten::Conf::Env`](pathname:///api/0.1/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.4/Marten.html#env-class-method) method, which returns a [`Marten::Conf::Env`](pathname:///api/0.4/Marten/Conf/Env.html) object. For example: ```crystal Marten.env # => diff --git a/docs/versioned_docs/version-0.4/development/testing.md b/docs/versioned_docs/version-0.4/development/testing.md new file mode 100644 index 000000000..1ec7889d7 --- /dev/null +++ b/docs/versioned_docs/version-0.4/development/testing.md @@ -0,0 +1,253 @@ +--- +title: Testing +description: Learn how to test your Marten project. +sidebar_label: Testing +--- + +This section covers the basics regarding how to test a Marten project and the various tools that you can leverage in this regard. + +## The basics + +You should test your Marten project to ensure that it adheres to the specifications it was built for. Like any Crystal project, Marten lets you write "specs" (see the [official documentation related to testing in Crystal](https://crystal-lang.org/reference/guides/testing.html) to learn more about those). + +By default, when creating a project through the use of the [`new`](./reference/management-commands.md#new) management command, Marten will automatically create a `spec/` folder at the root of your project structure. This folder contains a unique `spec_helper.cr` file allowing you to initialize the test environment for your Marten project. + +This file should look something like this: + +```crystal title=spec/spec_helper.cr +ENV["MARTEN_ENV"] = "test" + +require "spec" +require "marten" +require "marten/spec" + +require "../src/project" +``` + +As you can see, the `spec_helper.cr` file forces the Marten environment variable to be set to `test` and requires the spec library as well as Marten and your actual project. This file should be required by all your spec files. + +:::info +It's very important to require `marten/spec` in your top-level spec helper as this will ensure that the mandatory spec callbacks are configured for your spec suite (eg. in order to ensure that your database is properly set up before each spec is executed). +::: + +When it comes to running your tests, you can simply make use of the standard [`crystal spec`](https://crystal-lang.org/reference/man/crystal/index.html#crystal-spec) command. + +## Writing tests + +To write tests, you should write regular [specs](https://crystal-lang.org/reference/guides/testing.html) and ensure that your spec files always require the `spec/spec_helper.cr` file. + +For example: + +```crystal +require "./spec_helper" + +describe MySuperAbstraction do + describe "#foo" do + it "returns bar" do + obj = MySuperAbstraction.new + obj.foo.should eq "bar" + end + end +end +``` + +You are encouraged to organize your spec files by following the structure of your projects. For example, you could create a `models` folder and define specs related to your models in it. + +:::tip +When organizing spec files across multiple folders, one good practice is to define a `spec_helper.cr` file at each level of your folders structure. These additional `spec_helper.cr` files should require the same file from the parent folder. + +For example: + +```crystal title=spec/models/spec_helper.cr +require "../spec_helper" +``` + +```crystal title=spec/models/article_spec.cr +require "./spec_helper" + +describe Article do + # ... +end +``` +::: + +## Running tests + +As mentioned before, running specs involves making use of the standard [`crystal spec`](https://crystal-lang.org/reference/man/crystal/index.html#crystal-spec) command. + +### The test environment + +By default, the [`new`](./reference/management-commands.md#new) management command always creates a `test` environment when generating new projects. As such, you should ensure that the `MARTEN_ENV` environment variable is set to `test` when running your Crystal specs. It should also be reminded that this `test` environment is associated with a dedicated settings file where test-related settings can be specified and/or overridden if necessary (see [Settings](./settings.md#environments) for more details about this). + +### The test database + +Marten **must** use a different database when running tests in order to not tamper with your regular database. Indeed, the database used in the context of specs will be flushed and generated automatically every time the specs suite is executed. You should not set these database names to the same names as the ones used for your development or production environments. If test database names are not explicitly set, your specs suite won't be allowed to run at all. + +One way to ensure you use a dedicated database specifically for tests is to override the [`database`](./reference/settings.md#database-settings) settings as follows: + +```crystal title=config/settings/test.cr +Marten.configure :test do |config| + config.database do |db| + db.name = "my_project_test" + end +end +``` + +## Testing tools + +Marten provides some tools that can become useful when writing specs. + +### Using the test client + +The test client is an abstraction that is provided when requiring `marten/spec` and that acts as a very basic web client. This tool allows you to easily test your handlers and the various routes of your application by issuing requests and by introspecting the returned responses. + +By leveraging the test client, you can easily simulate various requests (eg. GET or POST requests) for specific URLs and observe the returned responses. While doing so, you can introspect the response properties (such as its status code, content, and headers) in order to verify that your handlers behave as expected. + +#### A simple example + +To use the test client, you can either initialize a [`Marten::Spec::Client`](pathname:///api/0.4/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.4/Marten/Spec.html#client%3AClient-class-method) method. Initializing new [`Marten::Spec::Client`](pathname:///api/0.4/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.4/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: + +```crystal +describe MyRedirectHandler do + describe "#get" do + it "returns the expected redirect response" do + response = Marten::Spec.client.get("/my-redirect-handler", query_params: {"foo" => "bar"}) + + response.status.should eq 302 + response.headers["Location"].should eq "/redirected" + end + end +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.4/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) +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.4/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.4/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.4/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.4/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 + describe "#post" do + it "validates the data and redirects" do + response = Marten::Spec.client.post("/my-schema-handler", data: {"first_name" => "John", "last_name" => "Doe"}) + + response.status.should eq 302 + response.headers["Location"].should eq "/redirected" + end + end +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.4/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.4/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 + +It is important to note that exceptions raised in your handlers will be visible from your spec. This means that you should use the standard [`#expect_raises`](https://crystal-lang.org/api/Spec/Expectations.html#expect_raises%28klass%3AT.class%2Cmessage%3AString%7CRegex%7CNil%3Dnil%2Cfile%3D__FILE__%2Cline%3D__LINE__%2C%26%29forallT-instance-method) expectation helper to verify that these exceptions are indeed raised. + +#### 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.4/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.4/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: + +```crystal +describe MyHandler do + describe "#get" do + it "renders the expected content if the right value is in the session" do + Marten::Spec.client.session["foo"] = "bar" + + url = Marten.routes.reverse("initiate_request") + response = Marten::Spec.client.get(url) + + response.status.should eq 200 + response.content.includes?("Initiate request").should be_true + end + end +end +``` + +#### Testing client and authentication + +When using the [marten-auth](https://github.com/martenframework/marten-auth) shard and the built-in [authentication](../authentication.mdx), a few additional helpers can be leveraged in order to easily sign in/sign out users while using the test client: + +* The `#sign_in` method can be used to simulate the effect of a signed-in user. This means that the user ID will be persisted into the test client session and that requests issued with it will be associated with the considered user +* The `#sign_out` method can be used to ensure that any signed-in user is logged out and that the session is flushed + +For example: + +```crystal +describe MyHandler do + describe "#get" do + it "shows the profile page of the authenticated user" do + user = Auth::User.create!(email: "test@example.com") do |user + user.set_password("insecure") + end + + url = Marten.routes.reverse("auth:profile") + + Marten::Spec.client.sign_in(user) + response = Marten::Spec.client.get(url) + + response.status.should eq 200 + response.content.includes?("Profile").should be_true + end + end +end +``` + +### Collecting emails + +If your code is sending [emails](../emailing/introduction.md), you might want to test that these emails are sent as expected. To do that, you can leverage the [development emailing backend](../emailing/reference/backends.md#development-backend) to ensure that sent emails are collected as part of each spec execution. + +To do that, the emailing backend needs to be initialized with `collect_emails: true` when configuring the [`emailing.backend`](./reference/settings.md#backend-1) setting. For example: + +```crystal title=config/settings/test.cr +Marten.configure :test do |config| + config.backend = Marten::Emailing::Backend::Development.new(collect_emails: true) +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.4/Marten/Spec.html#delivered_emails%3AArray(Emailing%3A%3AEmail)-class-method) method, which returns an array of [`Marten::Email`](pathname:///api/0.4/Marten/Emailing/Email.html) instances. For example: + +```crystal +describe MyObject do + describe "#do_something" do + it "sends an email as expected" do + obj = MyObject.new + obj.do_something + + Marten::Spec.delivered_emails.size.should eq 1 + Marten::Spec.delivered_emails[0].subject.should eq "Test subject" + end + end +end +``` + +:::info +Note that Marten also automatically ensures that the collected emails are automatically reset after each spec execution so that you don't have to take care of that directly. +::: diff --git a/docs/versioned_docs/version-0.4/emailing.mdx b/docs/versioned_docs/version-0.4/emailing.mdx new file mode 100644 index 000000000..abf593bd8 --- /dev/null +++ b/docs/versioned_docs/version-0.4/emailing.mdx @@ -0,0 +1,34 @@ +--- +title: Emailing +--- + +import DocCard from '@theme/DocCard'; + +Marten provides a convenient email definition mechanism that leverages templates. These emails are sent through the use of a generic emailing backend system that can be used to fully customize how emails are delivered. + +## Guides + +
+
+ +
+
+ +
+
+ +## How-to's + +
+
+ +
+
+ +## Reference + +
+
+ +
+
diff --git a/docs/versioned_docs/version-0.4/emailing/callbacks.md b/docs/versioned_docs/version-0.4/emailing/callbacks.md new file mode 100644 index 000000000..26b134807 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.4/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.4/Marten/Emailing/Email.html#deliver-instance-method), [`#html_body`](pathname:///api/0.4/Marten/Emailing/Email.html#html_body%3AString|Nil-instance-method), or [`#text_body`](pathname:///api/0.4/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.4/emailing/how-to/create-custom-emailing-backends.md b/docs/versioned_docs/version-0.4/emailing/how-to/create-custom-emailing-backends.md new file mode 100644 index 000000000..6f3fea07a --- /dev/null +++ b/docs/versioned_docs/version-0.4/emailing/how-to/create-custom-emailing-backends.md @@ -0,0 +1,30 @@ +--- +title: Create emailing backends +description: How to create custom emailing backends. +--- + +Marten lets you easily create custom [emailing backends](../introduction.md#emailing-backends) that you can then use as part of your application when it comes to sending emails. + +## 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.4/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.4/Marten/Emailing/Email.html)), corresponding to the email to deliver. + +For example: + +```crystal +class CustomEmailingBackend < Marten::Emailing::Backend::Base + def deliver(email : Email) + # Deliver the email! + end +end +``` + +## Enabling the use of custom emailing backends + +Custom emailing backends can be used by assigning an instance of the corresponding class to the [`emailing.backend`](../../development/reference/settings.md#backend-1) setting. + +For example: + +```crystal +config.emailing.backend = CustomEmailingBackend.new +``` diff --git a/docs/versioned_docs/version-0.4/emailing/introduction.md b/docs/versioned_docs/version-0.4/emailing/introduction.md new file mode 100644 index 000000000..e5cfb4132 --- /dev/null +++ b/docs/versioned_docs/version-0.4/emailing/introduction.md @@ -0,0 +1,170 @@ +--- +title: Introduction to emails +description: Learn how to define emails in a Marten project and how to deliver them. +sidebar_label: Introduction +--- + +Marten lets you define emails in a very declarative way and gives you the ability to fully customize the content of these emails, their properties and associated header values, and obviously how they should be delivered. + +## Email definition + +Emails must be defined as subclasses of the [`Emailing::Email`](pathname:///api/0.4/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: + +```crystal +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + def initialize(@user : User) + end +end +``` + +:::info +It is not necessary to systematically specify the `from` email address with the [`#from`](pathname:///api/0.4/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.4/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.4/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.4/Marten/Emailing/Email.html#from(value)-macro) macro) and the subject of the email ([`#subject`](pathname:///api/0.4/Marten/Emailing/Email.html#subject(value)-macro) macro). + +### Specifying email properties + +Most email properties (eg. from address, recipient addresses, etc) can be specified in two ways: + +* 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.4/Marten/Emailing/Email.html#from(value)-macro) for the sender email, [`#to`](pathname:///api/0.4/Marten/Emailing/Email.html#to(value)-macro) for the recipient addresses, [`#cc`](pathname:///api/0.4/Marten/Emailing/Email.html#cc(value)-macro) for the CC addresses, [`#bcc`](pathname:///api/0.4/Marten/Emailing/Email.html#bcc(value)-macro) for the BCC addresses, [`#reply_to`](pathname:///api/0.4/Marten/Emailing/Email.html#reply_to(value)-macro) for the Reply-To address, and [`#subject`](pathname:///api/0.4/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: + +```crystal +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + template_name "emails/welcome_email.html" + + def initialize(@user : User) + end + + def subject + if @user.referred? + "Glad to see you here!" + else + "Welcome to the app!" + end + end +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.4/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 + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html", content_type: :html + template_name "emails/welcome_email.txt", content_type: :text + + def initialize(@user : User) + end +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.4/Marten/Emailing/Email.html#html_body%3AString%3F-instance-method) and [`#text_body`](pathname:///api/0.4/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.4/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. + +For example: + +```crystal +class WelcomeEmail < Marten::Email + to @user.email + template_name "emails/welcome_email.html" + + def initialize(@user : User) + end + + def headers + {"X-Foo" => "bar"} + end +end +``` + +## Sending emails + +Emails are sent _synchronously_ through the use of the [`#deliver`](pathname:///api/0.4/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.4/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.4/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.4/Marten/Emailing/Email.html#backend(backend%3ABackend%3A%3ABase)%3ANil-class-method) class method. For example: + +```crystal +class WelcomeEmail < Marten::Email + from "no-reply@martenframework.com" + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + backend Marten::Emailing::Backend::Development.new(print_emails: true) + + def initialize(@user : User) + end +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.4/emailing/reference/backends.md b/docs/versioned_docs/version-0.4/emailing/reference/backends.md new file mode 100644 index 000000000..31fb6d474 --- /dev/null +++ b/docs/versioned_docs/version-0.4/emailing/reference/backends.md @@ -0,0 +1,31 @@ +--- +title: Emailing backends +description: Emailing backends reference. +sidebar_label: Backends +--- + +## Built-in backends + +### Development backend + +This is the default backend used as part of the [`emailing.backend`](../../development/reference/settings.md#backend-1) setting. + +This backend "collects" all the emails that are "delivered", which can be used in specs in order to test sent emails. This "collect" behavior can be disabled if necessary, and the backend can also be configured to print email details to the standard output. + +For example: + +```crystal +config.emailing.backend = Marten::Emailing::Backend::Development.new(print_emails: true, collect_emails: false) +``` + +## Other backends + +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: + +* [`marten-smtp-emailing`](https://github.com/martenframework/marten-smtp-emailing) provides an SMTP emailing backend +* [`marten-sendgrid-emailing`](https://github.com/martenframework/marten-sendgrid-emailing) provides a [Sendgrid](https://sendgrid.com/) emailing backend +* [`marten-mailgun-emailing`](https://github.com/martenframework/marten-mailgun-emailing) provides a [Mailgun](https://www.mailgun.com/) emailing backend + +:::info +Feel free to contribute to this page and add links to your shards if you've created emailing backends that are not listed here! +::: diff --git a/docs/versioned_docs/version-0.1/files.mdx b/docs/versioned_docs/version-0.4/files.mdx similarity index 62% rename from docs/versioned_docs/version-0.1/files.mdx rename to docs/versioned_docs/version-0.4/files.mdx index d44af57d6..1ec2b505c 100644 --- a/docs/versioned_docs/version-0.1/files.mdx +++ b/docs/versioned_docs/version-0.4/files.mdx @@ -4,7 +4,7 @@ title: Files import DocCard from '@theme/DocCard'; -Dealing with files is a common requirement for web applications. This section covers how to handle uploaded files and static assets. +Dealing with files is a common requirement for web applications. This section covers how to upload and manage files. ## Guides @@ -15,7 +15,12 @@ Dealing with files is a common requirement for web applications. This section co
-
- + + +## How-to's + +
+
+
diff --git a/docs/versioned_docs/version-0.4/files/how-to/create-custom-file-storages.md b/docs/versioned_docs/version-0.4/files/how-to/create-custom-file-storages.md new file mode 100644 index 000000000..9f9240015 --- /dev/null +++ b/docs/versioned_docs/version-0.4/files/how-to/create-custom-file-storages.md @@ -0,0 +1,78 @@ +--- +title: Create custom file storages +description: Learn how to create custom 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. You can leverage this capability to implement custom file storages (which you can then use for [assets](../../assets/introduction.md) or as part of [file model fields](../uploading-files.md#persisting-uploaded-files-in-model-records)). + +## Basic file storage implementation + +File storages are implemented as subclasses of the [`Marten::Core::Storage::Base`](pathname:///api/0.4/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.4/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) +* deleting files ([`#delete`](pathname:///api/0.4/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) +* opening files ([`#open`](pathname:///api/0.4/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) +* verifying that files exist ([`#exist?`](pathname:///api/0.4/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) +* retrieving file sizes ([`#size`](pathname:///api/0.4/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) +* retrieving file URLs ([`#url`](pathname:///api/0.4/Marten/Core/Storage/Base.html#url(filepath%3AString)%3AString-instance-method)) + +Note that you can fully customize how file storage objects are initialized. + +For example, a custom-made "file system" storage (that reads and writes files in a specific folder of the local file system) could be implemented as follows: + +```crystal +require "file_utils" + +class FileSystem < Marten::Core::Storage::Base + def initialize(@root : String, @base_url : String) + end + + def delete(filepath : String) : Nil + File.delete(path(filepath)) + rescue File::NotFoundError + raise Marten::Core::Storage::Errors::FileNotFound.new("File '#{filepath}' cannot be found") + end + + def exists?(filepath : String) : Bool + File.exists?(path(filepath)) + end + + def open(filepath : String) : IO + File.open(path(filepath), mode: "rb") + rescue File::NotFoundError + raise Marten::Core::Storage::Errors::FileNotFound.new("File '#{filepath}' cannot be found") + end + + def size(filepath : String) : Int64 + File.size(path(filepath)) + end + + def url(filepath : String) : String + File.join(base_url, URI.encode_path(filepath)) + end + + def write(filepath : String, content : IO) : Nil + new_path = path(filepath) + + FileUtils.mkdir_p(Path[new_path].dirname) + + File.open(new_path, "wb") do |new_file| + IO.copy(content, new_file) + end + end + + private getter root + private getter base_url + + private def path(filepath) + File.join(root, filepath) + end +end +``` + +## Using custom file storages + +You have many options when it comes to using your custom file storage classes, and those depend on what you are trying to do: + +* if you want to use a custom storage for [assets](../../assets/introduction.md), then you will likely want to assign an instance of your custom storage class to the [`assets.storage`](../../development/reference/settings.md#storage) setting (see [Assets storage](../../assets/introduction.md#assets-storage) to learn more about assets storages specifically) +* if you want to use a custom storage for all your [file model fields](../../models-and-databases/reference/fields.md#file), then you will likely want to assign an instance of your custom storage class to the [`media_files.storage`](../../development/reference/settings.md#storage-1) setting (see [File storages](../managing-files.md#file-storages) to learn more about file storages specifically) diff --git a/docs/versioned_docs/version-0.1/files/managing-files.md b/docs/versioned_docs/version-0.4/files/managing-files.md similarity index 90% rename from docs/versioned_docs/version-0.1/files/managing-files.md rename to docs/versioned_docs/version-0.4/files/managing-files.md index e29255a30..0c1d3f059 100644 --- a/docs/versioned_docs/version-0.1/files/managing-files.md +++ b/docs/versioned_docs/version-0.4/files/managing-files.md @@ -30,7 +30,7 @@ attachment.file.size # => 5796929 attachment.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.1/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#file` method is a "file object": an instance of [`Marten::DB::Field::File::File`](pathname:///api/0.4/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. @@ -71,14 +71,14 @@ 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.1/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.4/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.1/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.1/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.4/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.4/Marten/DB/Field/File/File.html#committed%3F%3ABool-instance-method) method returns `true` For example: @@ -94,14 +94,14 @@ 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.1/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.4/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.1/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.4/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: @@ -113,7 +113,7 @@ 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.1/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.4/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: @@ -130,7 +130,7 @@ attachment.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.1/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.4/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: @@ -145,18 +145,18 @@ attachment.file.committed? # => false 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.1/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.4/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.1/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.4/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.1/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) -* deleting files ([`#delete`](pathname:///api/0.1/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) -* opening files ([`#open`](pathname:///api/0.1/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) -* verifying that files exist ([`#exist?`](pathname:///api/0.1/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) -* retrieving file sizes ([`#size`](pathname:///api/0.1/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) -* retrieving file URLs ([`#url`](pathname:///api/0.1/Marten/Core/Storage/Base.html#url(filepath%3AString)%3AString-instance-method)) +* saving files ([`#save`](pathname:///api/0.4/Marten/Core/Storage/Base.html#save(filepath%3AString%2Ccontent%3AIO)%3AString-instance-method)) +* deleting files ([`#delete`](pathname:///api/0.4/Marten/Core/Storage/Base.html#delete(filepath%3AString)%3ANil-instance-method)) +* opening files ([`#open`](pathname:///api/0.4/Marten/Core/Storage/Base.html#open(filepath%3AString)%3AIO-instance-method)) +* verifying that files exist ([`#exist?`](pathname:///api/0.4/Marten/Core/Storage/Base.html#exists%3F(filepath%3AString)%3ABool-instance-method)) +* retrieving file sizes ([`#size`](pathname:///api/0.4/Marten/Core/Storage/Base.html#size(filepath%3AString)%3AInt64-instance-method)) +* retrieving file URLs ([`#url`](pathname:///api/0.4/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: @@ -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.1/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.4/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.1/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.4/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.1/files/uploading-files.md b/docs/versioned_docs/version-0.4/files/uploading-files.md similarity index 90% rename from docs/versioned_docs/version-0.1/files/uploading-files.md rename to docs/versioned_docs/version-0.4/files/uploading-files.md index 164490248..87c5d5c2b 100644 --- a/docs/versioned_docs/version-0.1/files/uploading-files.md +++ b/docs/versioned_docs/version-0.4/files/uploading-files.md @@ -8,7 +8,7 @@ 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.1/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.1/Marten/HTTP/Request.html)). These file objects are instances of the [`Marten::HTTP::UploadedFile`](pathname:///api/0.1/Marten/HTTP/UploadedFile.html) class. +Uploaded files are made available in the [`#data`](pathname:///api/0.4/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.4/Marten/HTTP/Request.html)). These file objects are instances of the [`Marten::HTTP::UploadedFile`](pathname:///api/0.4/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: @@ -21,7 +21,7 @@ class ProcessUploadedFileHandler < Marten::Handler end ``` -[`Marten::HTTP::UploadedFile`](pathname:///api/0.1/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.4/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 @@ -84,4 +84,4 @@ class UploadFileHandler < Marten::Handlers::Schema end ``` -Here, the `UploadFileHandler` inherits from the [`Marten::Handlers::Schema`](pathname:///api/0.1/Marten/Handlers/Schema.html) generic handler. It would also make sense to leverage the [`Marten::Handlers::RecordCreate`](pathname:///api/0.1/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.4/Marten/Handlers/Schema.html) generic handler. It would also make sense to leverage the [`Marten::Handlers::RecordCreate`](pathname:///api/0.4/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.1/getting-started.mdx b/docs/versioned_docs/version-0.4/getting-started.mdx similarity index 100% rename from docs/versioned_docs/version-0.1/getting-started.mdx rename to docs/versioned_docs/version-0.4/getting-started.mdx diff --git a/docs/versioned_docs/version-0.1/getting-started/installation.md b/docs/versioned_docs/version-0.4/getting-started/installation.md similarity index 65% rename from docs/versioned_docs/version-0.1/getting-started/installation.md rename to docs/versioned_docs/version-0.4/getting-started/installation.md index 62aec745e..cc44cbcfb 100644 --- a/docs/versioned_docs/version-0.1/getting-started/installation.md +++ b/docs/versioned_docs/version-0.4/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,9 +25,17 @@ 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 **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: @@ -36,11 +43,7 @@ Marten also has built-in support for PostgreSQL and MySQL. Please refer to the a * [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 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 +58,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 +77,7 @@ marten -v Marten can be installed from the sources by running the following commands: ```bash -git clone --branch v0.1.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.1/getting-started/tutorial.md b/docs/versioned_docs/version-0.4/getting-started/tutorial.md similarity index 88% rename from docs/versioned_docs/version-0.1/getting-started/tutorial.md rename to docs/versioned_docs/version-0.4/getting-started/tutorial.md index 537076eb0..e55a77f2b 100644 --- a/docs/versioned_docs/version-0.1/getting-started/tutorial.md +++ b/docs/versioned_docs/version-0.4/getting-started/tutorial.md @@ -44,6 +44,8 @@ myblog/ ├── spec │   └── spec_helper.cr ├── src +│   ├── assets +│   ├── emails │   ├── handlers │   ├── migrations │   ├── models @@ -52,6 +54,8 @@ myblog/ │   ├── cli.cr │   ├── project.cr │   └── server.cr +├── .editorconfig +├── .gitignore ├── manage.cr └── shard.yml ``` @@ -62,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 empty `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. | @@ -315,25 +320,35 @@ Templates provide a convenient way for defining the presentation logic of a web Let's define the expected template for our home handler by creating a `src/templates/home.html` file with the following content: ```html title="src/templates/home.html" -

My blog

-

Articles:

-
    -{% for article in articles %} -
  • {{ article.title }}
  • -{% endfor %} -
+{% extend "base.html" %} + +{% block content %} +

My blog

+

Articles:

+
    + {% for article in articles %} +
  • {{ article.title }}
  • + {% endfor %} +
+{% endblock %} ``` As you can see, Marten's templating system relies on variables that are surrounded by **`{{`** and **`}}`**. Each variable can involve lookups in order to access specific object attributes. In the above example `{{ article.title }}` means that the `title` attribute of the `article` variable should be outputted. Method-calling is done by using statements (also called "template tags") delimited by **`{%`** and **`%}`**. Such statements can involve for loops, if conditions, etc. In the above example we are using a for loop to iterate over the `Article` records in the `articles` query set that is "passed" to the template context in our `HomeHandler` handler. -If you go back to the home page ([http://localhost:8000](http://localhost:8000)), you should be able to see a list of article titles corresponding to all the `Article` records you created previously. - :::info Please refer to [Templates](../templates/introduction.md) to learn more about Marten's templating system. ::: +:::info +What about the `extend` and `block` tags in the previous snippet? These tags allow to "extend" a "base" template that usually contains the layout of an application (`base.html` in the above snippet) and to explicitly define the contents of the "blocks" that are expected by this base template. New marten projects are created with a simple `base.html` template that defines a very basic HTML document, whose body is filled with the content of a `content` block. This is why templates in this tutorial extend a `base.html` and override the content of the `content` block. + +You can learn more about these capabilities in [Template inheritance](../templates/introduction.md#template-inheritance). +::: + +If you go back to the home page ([http://localhost:8000](http://localhost:8000)), you should be able to see a list of article titles corresponding to all the `Article` records you created previously. + We have now pieced together the main components of the Marten web framework (Models, Handlers, Templates). When accessing the home page of our application, the following steps are taken care of by the framework: 1. the browser issues a GET request to `http://localhost:8000` @@ -385,8 +400,12 @@ As you can see above, the new route we mapped to the `ArticleDetailHandler` hand Obviously, we also need to define the `article_detail.html` template. To do so, let's create a `src/templates/article_detail.html` with the following content: ```html title="src/templates/article_detail.html" -

{{ article.title }}

-

{{ article.content }}

+{% extend "base.html" %} + +{% block content %} +

{{ article.title }}

+

{{ article.content }}

+{% endblock %} ``` Now if you try to access [http://localhost:8000/article/1](http://localhost:8000/article/1), you will be able to see the content of the `Article` record with ID 1. @@ -394,20 +413,24 @@ Now if you try to access [http://localhost:8000/article/1](http://localhost:8000 There is something missing though: the home page does not link to the "detail" page of each article. To remediate this, we can modify the `src/templates/home.html` template file as follows: ```html title="src/templates/home.html" -

My blog

-

Articles:

-
    -{% for article in articles %} -// highlight-next-line -
  • -// highlight-next-line - {{ article.title }} -// highlight-next-line - ‐ View -// highlight-next-line -
  • -{% endfor %} -
+{% extend "base.html" %} + +{% block content %} +

My blog

+

Articles:

+
    + {% for article in articles %} + // highlight-next-line +
  • + // highlight-next-line + {{ article.title }} + // highlight-next-line + ‐ View + // highlight-next-line +
  • + {% endfor %} +
+{% endblock %} ``` The `url` tag used in the above snippet allows to perform a reverse URL resolution. This allows to generate the final URL associated with a specific route name (the `article_detail` route name we defined earlier in this case). This reverse resolution can involve parameters if the considered route require ones. @@ -468,26 +491,24 @@ In the above snippet, we make use of `#redirect` in order to indicate that we wa We can now create the `article_create.html` template file with the following content: ```html title="src/templates/article_create.html" -

Create a new article

-
- +{% extend "base.html" %} + +{% block content %} +

Create a new article

+ + -
- {% for error in schema.title.errors %}

{{ error.message }}

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

{{ error.message }}

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

{{ error.message }}

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

{{ error.message }}

{% endfor %} -
- -
-
+
+ +{% endblock %} ``` As you can see, the above snippet defines a form that includes two fields: one for the `title` schema field and the other one for the `content` schema field. Each schema field can be errored depending on the result of a validation, and this is why specific field errors are (optionally) displayed as well. @@ -517,19 +538,22 @@ Now if you open your browser at [http://localhost:8000/article/create](http://lo Obviously, we still need to a link somewhere in our application to be able to easily access the article creation form. In this light, we can modify the `home.html` template file as follows: ```html title="src/templates/home.html" -

My blog

-// highlight-next-line -Create new article -

Articles:

-
    -{% for article in articles %} -
  • - {{ article.title }} - ‐ View -
  • -{% endfor %} -
+{% extend "base.html" %} +{% block content %} +

My blog

+ // highlight-next-line + Create new article +

Articles:

+
    + {% for article in articles %} +
  • + {{ article.title }} + ‐ View +
  • + {% endfor %} +
+{% endblock %} ``` :::info @@ -581,26 +605,24 @@ Here the `#get` and `#post` method implementations look similar to what was int We can now create the `article_update.html` template file with the following content: ```html title="src/templates/article_update.html" -

Update article "{{ article.title }}"

-
- +{% extend "base.html" %} + +{% block content %} +

Update article "{{ article.title }}"

+ + -
- {% for error in schema.title.errors %}

{{ error.message }}

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

{{ error.message }}

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

{{ error.message }}

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

{{ error.message }}

{% endfor %} -
- -
-
+
+ +{% endblock %} ``` As you can see, this looks very similar to what we did previously with the `article_create.html` template file. @@ -627,19 +649,23 @@ Now if you open your browser at [http://localhost:8000/article/1/update](http:// We can also add a link somewhere in the home page of the application to be able to easily access the update form for existing articles. In this light, we can modify the `home.html` template file as follows: ```html title="src/templates/home.html" -

My blog

-Create new article -

Articles:

-
    -{% for article in articles %} -
  • - {{ article.title }} - ‐ View - // highlight-next-line - ‐ Update -
  • -{% endfor %} -
+{% extend "base.html" %} + +{% block content %} +

My blog

+ Create new article +

Articles:

+
    + {% for article in articles %} +
  • + {{ article.title }} + ‐ View + // highlight-next-line + ‐ Update +
  • + {% endfor %} +
+{% endblock %} ``` ## Deleting an article @@ -674,12 +700,16 @@ In the above snippet, the `#get` method simply fetches the `Article` record by u Let's now create the `article_delete.html` template file with the following content: ```html title="src/templates/article_delete.html" -

Delete article "{{ article.title }}"

-

Are you sure?

-
- - -
+{% extend "base.html" %} + +{% block content %} +

Delete article "{{ article.title }}"

+

Are you sure?

+
+ + +
+{% endblock %} ``` This template simply asks the user for confirmation and displays a confirmation button embedded in a form to issue the POST request that will actually delete the record. @@ -707,20 +737,24 @@ Now if you open your browser at [http://localhost:8000/article/1/delete](http:// We can also add a link somewhere in the home page of the application to be able to easily access the delete confirmation page for existing articles. In this light, we can modify the `home.html` template file as follows: ```html title="src/templates/home.html" -

My blog

-Create new article -

Articles:

-
    -{% for article in articles %} -
  • - {{ article.title }} - ‐ View - ‐ Update - // highlight-next-line - ‐ Delete -
  • -{% endfor %} -
+{% extend "base.html" %} + +{% block content %} +

My blog

+ Create new article +

Articles:

+
    + {% for article in articles %} +
  • + {{ article.title }} + ‐ View + ‐ Update + // highlight-next-line + ‐ Delete +
  • + {% endfor %} +
+{% endblock %} ``` ## Refactoring: using template partials @@ -733,21 +767,15 @@ Let's create a `src/templates/partials/article_form.html` partial with the follo
-
-
- - {% for error in schema.title.errors %}

{{ error.message }}

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

{{ error.message }}

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

{{ error.message }}

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

{{ error.message }}

{% endfor %} -
- -
+
``` @@ -756,13 +784,21 @@ This partial template contains the exact same form that we used in the creation Let's now make use of this partial in the `src/templates/article_create.html` and `src/templates/article_update.html` templates: ```html title="src/templates/article_create.html" -

Create a new article

-{% include "partials/article_form.html" %} +{% extend "base.html" %} + +{% block content %} +

Create a new article

+ {% include "partials/article_form.html" %} +{% endblock %} ``` ```html title="src/templates/article_update.html" -

Update article "{{ article.title }}"

-{% include "partials/article_form.html" %} +{% extend "base.html" %} + +{% block content %} +

Update article "{{ article.title }}"

+ {% include "partials/article_form.html" %} +{% endblock %} ``` As you can see, the creation and update templates are now much more simple. @@ -773,7 +809,7 @@ The handlers we implemented previously map to common web development use cases: 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.1/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.4/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 @@ -785,7 +821,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.1/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.4/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 @@ -797,7 +833,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.1/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.4/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 @@ -810,7 +846,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.1/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.4/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 @@ -824,7 +860,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.1/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.4/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.1/handlers-and-http.mdx b/docs/versioned_docs/version-0.4/handlers-and-http.mdx similarity index 84% rename from docs/versioned_docs/version-0.1/handlers-and-http.mdx rename to docs/versioned_docs/version-0.4/handlers-and-http.mdx index 12a5613ca..de4f76bdf 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http.mdx +++ b/docs/versioned_docs/version-0.4/handlers-and-http.mdx @@ -21,11 +21,17 @@ Handlers are classes that process a web request and return a response. They impl
-
- +
+ +
+
+ +
+
+
- +
diff --git a/docs/versioned_docs/version-0.4/handlers-and-http/callbacks.md b/docs/versioned_docs/version-0.4/handlers-and-http/callbacks.md new file mode 100644 index 000000000..0069934f1 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.4/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.4/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.4/Marten/Handlers/Schema.html), but also handlers that inherit from [`Marten::Handlers::RecordCreate`](pathname:///api/0.4/Marten/Handlers/RecordCreate.html) and [`Marten::Handlers::RecordUpdate`](pathname:///api/0.4/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.4/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.4/handlers-and-http/cookies.md b/docs/versioned_docs/version-0.4/handlers-and-http/cookies.md new file mode 100644 index 000000000..a0ac23d08 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.4/Marten/Handlers/Cookies.html#cookies(*args%2C**options)-instance-method) method. +* [`Marten::HTTP::Request`](pathname:///api/0.4/Marten/HTTP/Request.html) objects give access to the cookies associated with the request via the [`#cookies`](pathname:///api/0.4/Marten/HTTP/Request.html#cookies-instance-method) method. +* [`Marten::HTTP::Response`](pathname:///api/0.4/Marten/HTTP/Response.html) objects give access to the cookies that will be returned with the HTTP response via the [`#cookies`](pathname:///api/0.4/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.4/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.4/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.4/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.4/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.4/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.4/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.4/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.4/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.4/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.4/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.4/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.1/handlers-and-http/error-handlers.md b/docs/versioned_docs/version-0.4/handlers-and-http/error-handlers.md similarity index 88% rename from docs/versioned_docs/version-0.1/handlers-and-http/error-handlers.md rename to docs/versioned_docs/version-0.4/handlers-and-http/error-handlers.md index 97f8901ce..8d699803c 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/error-handlers.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/Marten/Handlers/Defaults/PageNotFound.html) handler when: * a route cannot be found for an incoming request -* the [`Marten::HTTP::Errors::NotFound`](pathname:///api/0.1/Marten/HTTP/Errors/NotFound.html) exception is raised +* the [`Marten::HTTP::Errors::NotFound`](pathname:///api/0.4/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.1/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.4/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.1/Marten/Handlers/Defaults/BadRequest.html) handler when the [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.1/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.4/Marten/Handlers/Defaults/BadRequest.html) handler when the [`Marten::HTTP::Errors::SuspiciousOperation`](pathname:///api/0.4/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.1/Marten/Handlers/Defaults/PermissionDenied.html) handler when the [`Marten::HTTP::Errors::PermissionDenied`](pathname:///api/0.1/Marten/HTTP/Errors/PermissionDenied.html) exception is raised. +A Forbidden (403) response is automatically returned by the [`Marten::Handlers::Defaults::PermissionDenied`](pathname:///api/0.4/Marten/Handlers/Defaults/PermissionDenied.html) handler when the [`Marten::HTTP::Errors::PermissionDenied`](pathname:///api/0.4/Marten/HTTP/Errors/PermissionDenied.html) exception is raised. ## Customizing error handlers diff --git a/docs/versioned_docs/version-0.1/handlers-and-http/generic-handlers.md b/docs/versioned_docs/version-0.4/handlers-and-http/generic-handlers.md similarity index 84% rename from docs/versioned_docs/version-0.1/handlers-and-http/generic-handlers.md rename to docs/versioned_docs/version-0.4/handlers-and-http/generic-handlers.md index 12ce87a82..44f80919a 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/generic-handlers.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.4/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.1/handlers-and-http/how-to/create-custom-route-parameters.md b/docs/versioned_docs/version-0.4/handlers-and-http/how-to/create-custom-route-parameters.md similarity index 76% rename from docs/versioned_docs/version-0.1/handlers-and-http/how-to/create-custom-route-parameters.md rename to docs/versioned_docs/version-0.4/handlers-and-http/how-to/create-custom-route-parameters.md index ab65a6115..97c1b6691 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/how-to/create-custom-route-parameters.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.1/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.1/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.4/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.4/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.4/Marten/Routing/Parameter/Base.html#dumps(value)%3A%3A%3AString%3F-instance-method) method) -The [`#loads`](pathname:///api/0.1/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.4/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.1/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.4/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.4/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.1/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.4/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.1/handlers-and-http/introduction.md b/docs/versioned_docs/version-0.4/handlers-and-http/introduction.md similarity index 64% rename from docs/versioned_docs/version-0.1/handlers-and-http/introduction.md rename to docs/versioned_docs/version-0.4/handlers-and-http/introduction.md index b6c45ecf3..4066a1bff 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/introduction.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.1/Marten/HTTP/Response.html) object. +Handlers are initialized from a [`Marten::HTTP::Request`](pathname:///api/0.4/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.4/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.1/Marten/HTTP/Request.html)) and is required to return an HTTP response object (instance of [`Marten::HTTP::Response`](pathname:///api/0.1/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.4/Marten/HTTP/Request.html)) and is required to return an HTTP response object (instance of [`Marten::HTTP::Response`](pathname:///api/0.4/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.1/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.1/Marten/HTTP/Params/Data.html)) containing the request data. | -| `#flash` | Returns a hash-like object (instance of [`Marten::HTTP::FlashStore`](pathname:///api/0.1/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.1/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.4/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.4/Marten/HTTP/Params/Data.html)) containing the request data. | +| `#flash` | Returns a hash-like object (instance of [`Marten::HTTP::FlashStore`](pathname:///api/0.4/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.4/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.1/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.1/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.4/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.4/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.1/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.4/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.1/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.1/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.4/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.4/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.1/Marten/HTTP/Response.html) class directly (or one of its subclasses such as [`Marten::HTTP::Response::Found`](pathname:///api/0.1/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.4/Marten/HTTP/Response.html) class directly (or one of its subclasses such as [`Marten::HTTP::Response::Found`](pathname:///api/0.4/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.1/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.4/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.1/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.4/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.4/Marten/HTTP/Response.html) object, the [`Marten::HTTP::Response::Streaming`](pathname:///api/0.4/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.4/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.4/Marten/HTTP/Response/Streaming.html) objects directly, or you can also leverage the [`#respond`](pathname:///api/0.4/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.4/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.1/handlers-and-http/middlewares.md b/docs/versioned_docs/version-0.4/handlers-and-http/middlewares.md similarity index 92% rename from docs/versioned_docs/version-0.1/handlers-and-http/middlewares.md rename to docs/versioned_docs/version-0.4/handlers-and-http/middlewares.md index a7f58151d..5ee0cda91 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/middlewares.md +++ b/docs/versioned_docs/version-0.4/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.1/Marten/Middleware.html) abstract class. They must implement a `#call` method that takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.1/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.1/Marten/HTTP/Response.html) object: +Middlewares are subclasses of the [`Marten::Middleware`](pathname:///api/0.4/Marten/Middleware.html) abstract class. They must implement a `#call` method that takes a request object (instance of [`Marten::HTTP::Request`](pathname:///api/0.4/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.4/Marten/HTTP/Response.html) object: ```crystal class TestMiddleware < Marten::Middleware diff --git a/docs/versioned_docs/version-0.1/handlers-and-http/reference/generic-handlers.md b/docs/versioned_docs/version-0.4/handlers-and-http/reference/generic-handlers.md similarity index 60% rename from docs/versioned_docs/version-0.1/handlers-and-http/reference/generic-handlers.md rename to docs/versioned_docs/version-0.4/handlers-and-http/reference/generic-handlers.md index 88ec11e10..b95827bc0 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/reference/generic-handlers.md +++ b/docs/versioned_docs/version-0.4/handlers-and-http/reference/generic-handlers.md @@ -7,7 +7,7 @@ This page provides a reference for all the available [generic handlers](../gener ## Creating a record -**Class:** [`Marten::Handlers::RecordCreate`](pathname:///api/0.1/Marten/Handlers/RecordCreate.html) +**Class:** [`Marten::Handlers::RecordCreate`](pathname:///api/0.4/Marten/Handlers/RecordCreate.html) Handler allowing to create a new model record by processing a schema. @@ -24,13 +24,17 @@ 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.1/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.1/Marten/Handlers/Schema.html#schema(schema%3AMarten%3A%3ASchema.class%3F)-class-method) class method. Alternatively, the [`#schema_class`](pathname:///api/0.1/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 model class used to create the new record can be configured through the use of the [`#model`](pathname:///api/0.4/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.4/Marten/Handlers/Schema.html#schema(schema_klass)-macro) macro. Alternatively, the [`#schema_class`](pathname:///api/0.4/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.1/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.1/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.1/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.1/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. +The [`#template_name`](pathname:///api/0.4/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.4/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.4/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.4/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::RecordCreate`](pathname:///api/0.4/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.1/Marten/Handlers/RecordDelete.html) +**Class:** [`Marten::Handlers::RecordDelete`](pathname:///api/0.4/Marten/Handlers/RecordDelete.html) Handler allowing to delete a specific model record. @@ -46,11 +50,11 @@ end It should be noted that the redirect response issued will be a 302 (found). -The [`#template_name`](pathname:///api/0.1/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.1/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.1/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.1/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. +The [`#template_name`](pathname:///api/0.4/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.4/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.4/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.4/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.1/Marten/Handlers/RecordDetail.html) +**Class:** [`Marten::Handlers::RecordDetail`](pathname:///api/0.4/Marten/Handlers/RecordDetail.html) Handler allowing to display a specific model record. @@ -63,13 +67,13 @@ class ArticleDetailHandler < Marten::Handlers::RecordDetail end ``` -The model class used to retrieve the record can be configured through the use of the [`#model`](pathname:///api/0.1/Marten/Handlers/RecordDetail.html#model%3ADB%3A%3AModel.class%3F-class-method) class method. By default, a [`Marten::Handlers::RecordDetail`](pathname:///api/0.1/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.1/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.1/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. +The model class used to retrieve the record can be configured through the use of the [`#model`](pathname:///api/0.4/Marten/Handlers/RecordRetrieving.html#model(model_klass)-macro) macro. By default, a [`Marten::Handlers::RecordDetail`](pathname:///api/0.4/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.4/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.4/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. -The [`#template_name`](pathname:///api/0.1/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.1/Marten/Handlers/RecordDetail.html#record_context_name(name%3AString|Symbol)-class-method) class method. +The [`#template_name`](pathname:///api/0.4/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.4/Marten/Handlers/RecordDetail.html#record_context_name(name%3AString|Symbol)-class-method) class method. ## Listing records -**Class:** [`Marten::Handlers::RecordList`](pathname:///api/0.1/Marten/Handlers/RecordList.html) +**Class:** [`Marten::Handlers::RecordList`](pathname:///api/0.4/Marten/Handlers/RecordList.html) Handler allowing to list model records. @@ -82,11 +86,11 @@ class MyHandler < Marten::Handlers::RecordList end ``` -The model class used to retrieve the records can be configured through the use of the [`#model`](pathname:///api/0.1/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.1/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. +The model class used to retrieve the records can be configured through the use of the [`#model`](pathname:///api/0.4/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.4/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. -The [`#template_name`](pathname:///api/0.1/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.1/Marten/Handlers/RecordList.html#list_context_name(name%3AString|Symbol)-class-method) class method. +The [`#template_name`](pathname:///api/0.4/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.4/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.1/Marten/Handlers/RecordListing/ClassMethods.html#page_size(page_size%3AInt32%3F)-instance-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.4/Marten/Handlers/RecordListing/ClassMethods.html#page_size(page_size%3AInt32%3F)-instance-method) class method: ```crystal class MyHandler < Marten::Handlers::RecordList @@ -96,12 +100,19 @@ class MyHandler < Marten::Handlers::RecordList end ``` -When records are paginated, a [`Marten::DB::Query::Page`](pathname:///api/0.1/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.1/Marten/Handlers/RecordListing/ClassMethods.html#page_number_param(param%3AString|Symbol)-instance-method) class method. +When records are paginated, a [`Marten::DB::Query::Page`](pathname:///api/0.4/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.4/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.1/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.1/Marten/Handlers/RecordListing.html#queryset-instance-method) method and by applying additional filters to the default query set. +By default, handlers that inherit from [`Marten::Handlers::RecordList`](pathname:///api/0.4/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.4/Marten/Handlers/RecordListing.html#queryset(queryset)-macro) macro instead of the [`#model`](pathname:///api/0.4/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 +``` -For example: +Alternatively, it is also possible to override the [`#queryset`](pathname:///api/0.4/Marten/Handlers/RecordListing.html#queryset-instance-method) method and apply additional filters to the default query set: ```crystal class MyHandler < Marten::Handlers::RecordList @@ -117,7 +128,7 @@ end ## Updating a record -**Class:** [`Marten::Handlers::RecordUpdate`](pathname:///api/0.1/Marten/Handlers/RecordUpdate.html) +**Class:** [`Marten::Handlers::RecordUpdate`](pathname:///api/0.4/Marten/Handlers/RecordUpdate.html) Handler allowing to update a model record by processing a schema. @@ -134,19 +145,23 @@ 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.1/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.1/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.1/Marten/Handlers/RecordRetrieving/ClassMethods.html#lookup_param(lookup_param%3AString|Symbol)-instance-method) class method. +The model class used to update the new record can be configured through the use of the [`#model`](pathname:///api/0.4/Marten/Handlers/RecordRetrieving.html#model(model_klass)-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.4/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.4/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.4/Marten/Handlers/Schema.html#schema(schema_klass)-macro) macro. Alternatively, the [`#schema_class`](pathname:///api/0.4/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 schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.1/Marten/Handlers/Schema.html#schema(schema%3AMarten%3A%3ASchema.class%3F)-class-method) class method. Alternatively, the [`#schema_class`](pathname:///api/0.1/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.4/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.4/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.4/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.4/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. -The [`#template_name`](pathname:///api/0.1/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.1/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.1/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.1/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.4/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. +::: ## Performing a redirect -**Class:** [`Marten::Handlers::Redirect`](pathname:///api/0.1/Marten/Handlers/Redirect.html) +**Class:** [`Marten::Handlers::Redirect`](pathname:///api/0.4/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.1/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.1/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.1/Marten/Handlers/Redirect.html#redirect_url-instance-method) method. +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.4/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.4/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.4/Marten/Handlers/Redirect.html#redirect_url-instance-method) method. ```crystal class TestRedirectHandler < Marten::Handlers::Redirect @@ -154,13 +169,13 @@ class TestRedirectHandler < Marten::Handlers::Redirect 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.1/Marten/Handlers/Redirect.html#permanent(permanent%3ABool)-class-method) class method. +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.4/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.1/Marten/Handlers/Redirect.html#forward_query_string(forward_query_string%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.4/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.1/Marten/Handlers/Schema.html) +**Class:** [`Marten::Handlers::Schema`](pathname:///api/0.4/Marten/Handlers/Schema.html) Handler allowing to process a form through the use of a [schema](../../schemas.mdx). @@ -176,17 +191,21 @@ 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.1/Marten/Handlers/Schema.html#schema(schema%3AMarten%3A%3ASchema.class%3F)-class-method) class method. Alternatively, the [`#schema_class`](pathname:///api/0.1/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 schema used to perform the validation can be defined through the use of the [`#schema`](pathname:///api/0.4/Marten/Handlers/Schema.html#schema(schema_klass)-macro) macro. Alternatively, the [`#schema_class`](pathname:///api/0.4/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.1/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.1/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.1/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.1/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. +The [`#template_name`](pathname:///api/0.4/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.4/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.4/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.4/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.4/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.1/Marten/Handlers/Template.html) +**Class:** [`Marten::Handlers::Template`](pathname:///api/0.4/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.1/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method. +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.4/Marten/Handlers/Rendering/ClassMethods.html#template_name(template_name%3AString%3F)-instance-method) class method. ```crystal class HomeHandler < Marten::Handlers::Template @@ -194,14 +213,18 @@ 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. diff --git a/docs/versioned_docs/version-0.1/handlers-and-http/reference/middlewares.md b/docs/versioned_docs/version-0.4/handlers-and-http/reference/middlewares.md similarity index 51% rename from docs/versioned_docs/version-0.1/handlers-and-http/reference/middlewares.md rename to docs/versioned_docs/version-0.4/handlers-and-http/reference/middlewares.md index 2ed7788a6..4ae4749eb 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/reference/middlewares.md +++ b/docs/versioned_docs/version-0.4/handlers-and-http/reference/middlewares.md @@ -5,19 +5,47 @@ 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.4/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.4/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.4/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.4/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.1/Marten/Middleware/Flash.html) +**Class:** [`Marten::Middleware::Flash`](pathname:///api/0.4/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.1/Marten/Middleware/Session.html) in the [`middleware`](../../development/reference/settings.md#middleware) setting. +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.4/Marten/Middleware/Session.html) in the [`middleware`](../../development/reference/settings.md#middleware) setting. ## GZip middleware -**Class:** [`Marten::Middleware::GZip`](pathname:///api/0.1/Marten/Middleware/GZip.html) +**Class:** [`Marten::Middleware::GZip`](pathname:///api/0.4/Marten/Middleware/GZip.html) Compresses the content of the response if the browser supports GZip compression. @@ -25,17 +53,21 @@ This middleware will compress responses that are big enough (200 bytes or more) 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.1/Marten/Middleware/I18n.html) +**Class:** [`Marten::Middleware::I18n`](pathname:///api/0.4/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. +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. ## Session middleware -**Class:** [`Marten::Middleware::Session`](pathname:///api/0.1/Marten/Middleware/Session.html) +**Class:** [`Marten::Middleware::Session`](pathname:///api/0.4/Marten/Middleware/Session.html) Enables the use of [sessions](../sessions.md). @@ -43,9 +75,17 @@ When this middleware is used, each request will have a session store initialized 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.4/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.1/Marten/Middleware/StrictTransportSecurity.html) +**Class:** [`Marten::Middleware::StrictTransportSecurity`](pathname:///api/0.4/Marten/Middleware/StrictTransportSecurity.html) Sets the Strict-Transport-Security header in the response if it wasn't already set. @@ -61,7 +101,7 @@ This is why the value of the [`strict_security_policy.max_age`](../../developmen ## X-Frame-Options middleware -**Class:** [`Marten::Middleware::XFrameOptions`](pathname:///api/0.1/Marten/Middleware/XFrameOptions.html) +**Class:** [`Marten::Middleware::XFrameOptions`](pathname:///api/0.4/Marten/Middleware/XFrameOptions.html) Sets the X-Frame-Options header in the response if it wasn't already set. diff --git a/docs/versioned_docs/version-0.1/handlers-and-http/routing.md b/docs/versioned_docs/version-0.4/handlers-and-http/routing.md similarity index 81% rename from docs/versioned_docs/version-0.1/handlers-and-http/routing.md rename to docs/versioned_docs/version-0.4/handlers-and-http/routing.md index 11749a2d8..f2fc2f0f4 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/routing.md +++ b/docs/versioned_docs/version-0.4/handlers-and-http/routing.md @@ -64,7 +64,7 @@ The following route parameter types are available: | Type | Description | | ----------- | ----------- | -| `str` | Matches any non-empty string (excluding the forward slash character **`/`**). This is the default parameter type used for untyped parameters (eg. ``). | +| `str` or `string` | Matches any non-empty string (excluding the forward slash character **`/`**). This is the default parameter type used for untyped parameters (eg. ``). | | `int` | Matches zero or any positive integer. These parameter values are always deserialized as `UInt64` objects. | | `path` | Matches any non-empty strings including forward slash characters (**`/`**). For example `foo/bar/xyz` could be matched by this parameter type. | | `slug` | Matches any string containing only ASCII letters, numbers, hyphen, and underscore characters. For example `my-first-project-01` could be matched by this parameter type. | @@ -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.1/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.1/Marten.html#routes-class-method) method: +"Reversing" a URL is as simple as calling the [`Marten::Routing::Map#reverse`](pathname:///api/0.4/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.4/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.1/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.4/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.1/handlers-and-http/sessions.md b/docs/versioned_docs/version-0.4/handlers-and-http/sessions.md similarity index 86% rename from docs/versioned_docs/version-0.1/handlers-and-http/sessions.md rename to docs/versioned_docs/version-0.4/handlers-and-http/sessions.md index 87845b519..65c21e3c7 100644 --- a/docs/versioned_docs/version-0.1/handlers-and-http/sessions.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.4/Marten/Middleware/Session.html) middleware class. How the session ID cookie is generated can also be tweaked by leveraging the following settings: @@ -27,11 +27,11 @@ How session data is actually persisted can be defined by configuring the right s 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. -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. +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.1/Marten/Middleware/Session.html) middleware is used, each HTTP request object will have a [`#session`](pathname:///api/0.1//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.1/Marten/HTTP/Session/Store/Base.html) and provides a hash-like interface: +When the [`Marten::Middleware::Session`](pathname:///api/0.4/Marten/Middleware/Session.html) middleware is used, each HTTP request object will have a [`#session`](pathname:///api/0.4//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.4/Marten/HTTP/Session/Store/Base.html) and provides a hash-like interface: ```crystal # Persisting values: diff --git a/docs/versioned_docs/version-0.1/i18n.mdx b/docs/versioned_docs/version-0.4/i18n.mdx similarity index 100% rename from docs/versioned_docs/version-0.1/i18n.mdx rename to docs/versioned_docs/version-0.4/i18n.mdx diff --git a/docs/versioned_docs/version-0.1/i18n/introduction.md b/docs/versioned_docs/version-0.4/i18n/introduction.md similarity index 90% rename from docs/versioned_docs/version-0.1/i18n/introduction.md rename to docs/versioned_docs/version-0.4/i18n/introduction.md index a78742ddd..3da1e5000 100644 --- a/docs/versioned_docs/version-0.1/i18n/introduction.md +++ b/docs/versioned_docs/version-0.4/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.1/models-and-databases.mdx b/docs/versioned_docs/version-0.4/models-and-databases.mdx similarity index 81% rename from docs/versioned_docs/version-0.1/models-and-databases.mdx rename to docs/versioned_docs/version-0.4/models-and-databases.mdx index b5dd92c2c..bb09bfb2c 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases.mdx +++ b/docs/versioned_docs/version-0.4/models-and-databases.mdx @@ -15,17 +15,23 @@ Models define what data can be persisted and manipulated by a Marten application
+
+ +
+
+ +
-
- +
+
@@ -46,6 +52,9 @@ Models define what data can be persisted and manipulated by a Marten application
+
+ +
diff --git a/docs/versioned_docs/version-0.1/models-and-databases/callbacks.md b/docs/versioned_docs/version-0.4/models-and-databases/callbacks.md similarity index 59% rename from docs/versioned_docs/version-0.1/models-and-databases/callbacks.md rename to docs/versioned_docs/version-0.4/models-and-databases/callbacks.md index e139c89d7..690a6be14 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/callbacks.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/callbacks.md @@ -4,15 +4,13 @@ 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 As stated above, callbacks are methods that will be called when specific events occur for a specific model instance. They need to be registered explicitly as part your model definitions. There are [many types of of callbacks](#available-callbacks), and it is possible to register "before" or "after" callbacks for most of these types. -For example: +Registering a callback is as simple as calling the right callback macro (eg. `#before_validation`) with a symbol of the name of the method to call when the callback is executed. For example: ```crystal class User < Marten::Model @@ -72,3 +70,43 @@ The use of the `#save` / `#save!` and the `#create` / `#create!` methods will tr `before_delete` callbacks are called before a record gets deleted while `after_delete` callbacks are called after. The use of the `#delete` method will trigger these callbacks. + +### `after_commit` + +`after_commit` callbacks are called after a record is created, updated, or deleted, but only after the corresponding SQL transaction has been committed to the database (which isn't the case for other `after_*` callbacks - See [Transactions](./transactions.md) for more details). For example: + +```crystal +after_commit :do_something +``` + +As mentioned previously, by default such callbacks will run in the context of record creations, updates, and deletions. That being said it is also possible to associate these callbacks with one or more specific actions only by using the `on` argument. For example: + +```crystal +after_commit :do_something, on: :create # Will run after creations only +after_commit :do_something, on: :update # Will run after updates only +after_commit :do_something, on: :update # Will run after saves (creations or updates) only +after_commit :do_something, on: :delete # Will run after deletions only +after_commit :do_something_else, on: [:create, :delete] # Will run after creations and deletions only +``` + +The actions supported by the `on` argument are `create`, `update`, `save`, and `delete`. + +### `after_rollback` + +`after_rollback` callbacks are called after a transaction is rolled back when a record is created, updated, or deleted. For example: + +```crystal +after_rollback :do_something +``` + +As mentioned previously, by default such callbacks will run in the context of record creations, updates, and deletions. That being said it is also possible to associate these callbacks with one or more specific actions only by using the `on` argument. For example: + +```crystal +after_rollback :do_something, on: :create # Will run after rolled back creations only +after_rollback :do_something, on: :update # Will run after rolled back updates only +after_rollback :do_something, on: :update # Will run after rolled back saves (creations or updates) only +after_rollback :do_something, on: :delete # Will run after rolled back deletions only +after_rollback :do_something_else, on: [:create, :delete] # Will run after rolled back creations and deletions only +``` + +The actions supported by the `on` argument are `create`, `update`, `save`, and `delete`. diff --git a/docs/versioned_docs/version-0.1/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 similarity index 97% rename from docs/versioned_docs/version-0.1/models-and-databases/how-to/create-custom-model-fields.md rename to docs/versioned_docs/version-0.4/models-and-databases/how-to/create-custom-model-fields.md index 2bc86b9f1..baac79533 100644 --- a/docs/versioned_docs/version-0.1/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 @@ -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.1/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.4/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.1/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.4/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.1/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.4/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 @@ -141,7 +141,7 @@ The `#from_db_result_set` method is supposed to return the read value into the r #### `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.1/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.4/Marten/DB/Management/Column/Base.html). For example, an "email" field could return a string column as part of its `#to_column` method: @@ -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.1/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.4/Marten/DB/Field/Base.html) is fairly simply and looks like this: ```crystal def initialize( diff --git a/docs/versioned_docs/version-0.1/models-and-databases/introduction.md b/docs/versioned_docs/version-0.4/models-and-databases/introduction.md similarity index 78% rename from docs/versioned_docs/version-0.1/models-and-databases/introduction.md rename to docs/versioned_docs/version-0.4/models-and-databases/introduction.md index e34b86510..7140d81a7 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/introduction.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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,13 +183,68 @@ 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.4/Marten/DB/Model/Table.html#with_timestamp_fields-macro) macro: + +```crystal +class Article < Marten::Model + // highlight-next-line + with_timestamp_fields + + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 +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.4/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 + // highlight-next-line + field :created_at, :date_time, auto_now_add: true + // highlight-next-line + field :updated_at, :date_time, auto_now: true + + field :id, :big_int, primary_key: true, auto: true + field :title, :string, max_size: 255 +end +``` + ## Multifields indexes and unique constraints Single model fields can be indexed or associated with a unique constraint _individually_ by leveraging the [`index`](./reference/fields.md#index) and [`unique`](./reference/fields.md#unique) field options. That being said, it is sometimes necessary to configure multifields indexes or unique constraints. ### Multifields indexes -Multifields indexes can be configured in a model by leveraging the [`#db_index`](pathname:///api/0.1/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.4/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: @@ -218,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.1/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.4/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: @@ -363,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](#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). -:::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. @@ -389,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.1/models-and-databases/migrations.md b/docs/versioned_docs/version-0.4/models-and-databases/migrations.md similarity index 98% rename from docs/versioned_docs/version-0.1/models-and-databases/migrations.md rename to docs/versioned_docs/version-0.4/models-and-databases/migrations.md index 23b54179d..427c6abf2 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/migrations.md +++ b/docs/versioned_docs/version-0.4/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.1/Marten/DB/Migration.html) base class. A basic migration will look something like this: +Migrations always inherit from the [`Marten::Migration`](pathname:///api/0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/models-and-databases/multiple-databases.md b/docs/versioned_docs/version-0.4/models-and-databases/multiple-databases.md similarity index 95% rename from docs/versioned_docs/version-0.1/models-and-databases/multiple-databases.md rename to docs/versioned_docs/version-0.4/models-and-databases/multiple-databases.md index ea6f8ef97..ebdfe234b 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/multiple-databases.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.4/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.1/models-and-databases/queries.md b/docs/versioned_docs/version-0.4/models-and-databases/queries.md similarity index 92% rename from docs/versioned_docs/version-0.1/models-and-databases/queries.md rename to docs/versioned_docs/version-0.4/models-and-databases/queries.md index e47190c70..5885a359a 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/queries.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/queries.md @@ -265,7 +265,7 @@ Article.filter { } ``` -### 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,7 +287,15 @@ 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 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` method: @@ -307,9 +315,13 @@ The double underscores notations can also be used in the context of joins. For e Article.join(:author__hometown).get(id: 42) ``` +:::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. +::: + ### 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.1/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.4/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 +342,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.1/Marten/DB/Query/Paginator.html#page(number%3AInt)-instance-method) method. Such pages are instances of [`Marten::DB::Query::Page`](pathname:///api/0.1/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.1/Marten/DB/Query/Page.html#previous_page%3F-instance-method), [`#previous_page_number`](pathname:///api/0.1/Marten/DB/Query/Page.html#previous_page_number-instance-method), [`#next_page?`](pathname:///api/0.1/Marten/DB/Query/Page.html#next_page%3F-instance-method), and [`#next_page_number`](pathname:///api/0.1/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.4/Marten/DB/Query/Paginator.html#page(number%3AInt)-instance-method) method. Such pages are instances of [`Marten::DB::Query::Page`](pathname:///api/0.4/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.4/Marten/DB/Query/Page.html#previous_page%3F-instance-method), [`#previous_page_number`](pathname:///api/0.4/Marten/DB/Query/Page.html#previous_page_number-instance-method), [`#next_page?`](pathname:///api/0.4/Marten/DB/Query/Page.html#next_page%3F-instance-method), and [`#next_page_number`](pathname:///api/0.4/Marten/DB/Query/Page.html#next_page_number-instance-method) methods). ## Updating records diff --git a/docs/versioned_docs/version-0.4/models-and-databases/raw-sql.md b/docs/versioned_docs/version-0.4/models-and-databases/raw-sql.md new file mode 100644 index 000000000..663342945 --- /dev/null +++ b/docs/versioned_docs/version-0.4/models-and-databases/raw-sql.md @@ -0,0 +1,99 @@ +--- +title: Performing raw SQL queries +description: Learn how to perform raw SQL queries. +sidebar_label: Raw SQL +--- + +Marten gives you the ability to execute raw SQL if the capabilities provided by [query sets](./queries.md) are not sufficient for the task at hand. When doing so, multiple solutions can be considered: you can either decide to perform raw queries that are mapped to actual model instances, or you can execute entirely custom SQL statements. + +## Performing raw queries + +It is possible to perform raw SQL queries and expect to have the corresponding records mapped to actual model instances. This is possible by using the [`#raw`](./reference/query-set.md#raw) query set method. + +For example, the following snippet would allow iterating over all the `Article` model records (by assuming that the corresponding database table is the `main_article` one): + +```crystal +Article.raw("select * from main_article").each do |article| + # Do something with `article` record +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.4/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. + +For example, the following query uses positional parameters: + +```crystal +Article.raw("SELECT * FROM articles WHERE title = ? and created_at > ?", "Hello World!", "2022-10-30") +``` + +And the following one uses named parameters: + +```crystal +Article.raw( + "SELECT * FROM articles WHERE title = :title and created_at > :created_at", + title: "Hello World!", + created_at: "2022-10-30" +) +``` + +:::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). + +As such, never - ever - do something like that: + +```crystal +Article.raw("SELECT * FROM articles WHERE title = '#{title}'") +``` + +And instead, do something like that: + +```crystal +Article.raw("SELECT * FROM articles WHERE 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. +::: + +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. + +## 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. + +Marten's DB connections are essentially wrappers around DB connections provided by the [crystal-db](https://github.com/crystal-lang/crystal-db) package. They can be opened, which allows you to essentially execute any query on the considered database. + +For example, the following snippet would open a connection to the default database and execute a simple query: + +```crystal +Marten::DB::Connection.default.open do |db| + db.scalar("SELECT 1") +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.4/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) + +db.open do |db| + db.scalar("SELECT 1") +end +``` +::: + +The [`#open`](pathname:///api/0.4/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| + db.exec "create table contacts (name varchar(30), age int)" +end +``` + +Please refer to [Crystal's official documentation on interacting with databases](https://crystal-lang.org/reference/database/index.html) to learn more about this low-level API. diff --git a/docs/versioned_docs/version-0.1/models-and-databases/reference/fields.md b/docs/versioned_docs/version-0.4/models-and-databases/reference/fields.md similarity index 77% rename from docs/versioned_docs/version-0.1/models-and-databases/reference/fields.md rename to docs/versioned_docs/version-0.4/models-and-databases/reference/fields.md index b5ca142d1..88d8a82aa 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/reference/fields.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/reference/fields.md @@ -84,6 +84,18 @@ 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. + ### `file` A `file` field allows persisting the reference to an uploaded file. @@ -150,6 +162,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: @@ -158,6 +227,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: @@ -166,6 +239,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). @@ -262,7 +343,7 @@ The `on_delete` argument allows to specify the deletion strategy to adopt when a * `:do_nothing`: 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 to explicitly prevent 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 +* `:protect`: this strategy allows to explicitly prevent 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. ### `one_to_one` diff --git a/docs/versioned_docs/version-0.1/models-and-databases/reference/migration-operations.md b/docs/versioned_docs/version-0.4/models-and-databases/reference/migration-operations.md similarity index 100% rename from docs/versioned_docs/version-0.1/models-and-databases/reference/migration-operations.md rename to docs/versioned_docs/version-0.4/models-and-databases/reference/migration-operations.md diff --git a/docs/versioned_docs/version-0.1/models-and-databases/reference/query-set.md b/docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md similarity index 65% rename from docs/versioned_docs/version-0.1/models-and-databases/reference/query-set.md rename to docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md index aa81e0f24..253325557 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/reference/query-set.md +++ b/docs/versioned_docs/version-0.4/models-and-databases/reference/query-set.md @@ -73,7 +73,7 @@ qset_2[2..6]? # returns a "sliced" query set ### `all` -Allows to retrieve 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. +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. For example: @@ -143,7 +143,7 @@ query_set.filter { (q(name: "Foo") | q(name: "Bar")) & q(is_published: True) } 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). -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 +155,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. @@ -175,7 +179,7 @@ query_set.none.exists? # => false ### `order` -Allows to specify the ordering in which records should be returned when evaluating the query set. +Allows specifying the ordering in which records should be returned when evaluating the query set. Multiple fields can be specified in order to define the final ordering. For example: @@ -186,9 +190,47 @@ 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). +### `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.4/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") +``` + +Additional parameters can also be specified if the query needs to be parameterized. Those can be specified as positional or named arguments. For example: + +```crystal +# Using splat positional parameters: +Article.all.raw("SELECT * FROM articles WHERE title = ? and created_at > ?", "Hello World!", "2022-10-30") + +# Using an array of positional parameters: +Article.all.raw("SELECT * FROM articles WHERE title = ? and created_at > ?", ["Hello World!", "2022-10-30"]) + +# Using double splat named parameters: +Article.all.raw( + "SELECT * FROM articles WHERE title = :title and created_at > :created_at", + title: "Hello World!", + created_at: "2022-10-30" +) + +# Using a hash of named parameters: +Article.all.raw( + "SELECT * FROM articles WHERE title = :title and created_at > :created_at", + { + title: "Hello World!", + created_at: "2022-10-30", + } +) +``` + +Please refer to [Raw SQL](../raw-sql.md) to learn more about performing raw SQL queries. + ### `reverse` -Allows to reverse the order of the current query set. +Allows reversing the order of the current query set. For example, this would return all the `Article` records ordered by descending title: @@ -199,7 +241,7 @@ query_set.reverse ### `using` -Allows to define which database alias should be used when evaluating the query set. +Allows defining which database alias should be used when evaluating the query set. For example: @@ -222,6 +264,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" ``` @@ -258,7 +301,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 @@ -280,7 +323,7 @@ Article.filter(title__startswith: "Top").delete # deletes all the articles whose ### `each` -Allows to iterate over the records that are targeted by the current query set. +Allows iterating over the records that are targeted by the current query set. This method can be used to define a block that iterates over the records that are targeted by a query set: @@ -300,6 +343,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. @@ -362,6 +413,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. @@ -394,6 +501,45 @@ paginator = query_set.paginator(10) paginator.page(1) # Returns the first page of records ``` +### `pick` + +Returns specific column values for a single record without actually loading it. + +This method allows to easily select specific column values for a single record from the current query set. This allows retrieving specific column values without actually loading the entire record, and as such this is most useful for query sets that have been narrowed down to match a single record. The method returns an array containing the requested column values, or `nil` if no record was matched by the current query set. + +For example: + +```crystal +Post.filter(pk: 1).pick("title", "published") +# => ["First article", true] +``` + +### `pick!` + +Returns specific column values for a single record without actually loading it. + +This method allows to easily select specific column values for a single record from the current query set. This allows retrieving specific column values without actually loading the entire record, and as such this is most useful for query sets that have been narrowed down to match a single record. The method returns an array containing the requested column values, or raises `NilAssertionError` if no record was matched by the current query set. + +For example: + +```crystal +Post.filter(pk: 1).pick!("title", "published") +# => ["First article", true] +``` + +### `pluck` + +Returns specific column values without loading entire record objects. + +This method allows to easily select specific column values from the current query set. This allows retrieving specific column values without actually loading entire records. The method returns an array containing one array with the actual column values for each record targeted by the query set. + +For example: + +```crystal +Post.all.pluck("title", "published") +# => [["First article", true], ["Upcoming article", false]] +``` + ### `size` Alias for [`#count`](#count): returns the number of records that are targetted by the query set. @@ -417,7 +563,7 @@ Below are listed all the available [field predicates](../queries.md#field-predic ### `contains` -Allows to filter records based on field values that contain a specific substring. Note that this is a **case-sensitive** predicate. +Allows filtering records based on field values that contain a specific substring. Note that this is a **case-sensitive** predicate. ```crystal Article.all.filter(title__contains: "tech") @@ -425,7 +571,7 @@ Article.all.filter(title__contains: "tech") ### `endswith` -Allows to filter records based on field values that end with a specific substring. Note that this is a **case-sensitive** predicate. +Allows filtering records based on field values that end with a specific substring. Note that this is a **case-sensitive** predicate. ```crystal Article.all.filter(title__endswith: "travel") @@ -433,7 +579,7 @@ Article.all.filter(title__endswith: "travel") ### `exact` -Allows to filter records based on a specific field value (exact match). Note that providing a `nil` value will result in a `IS NULL` check at the SQL level. +Allows filtering records based on a specific field value (exact match). Note that providing a `nil` value will result in a `IS NULL` check at the SQL level. This is the default predicate; as such it is not necessary to specify it when filtering records. The following two query sets are equivalent: @@ -444,7 +590,7 @@ Article.all.filter(published__exact: true) ### `gte` -Allows to filter record based on field values that are greater than or equal to a specified value. +Allows filtering records based on field values that are greater than or equal to a specified value. ```crystal Article.all.filter(rating__gte: 10) @@ -452,7 +598,7 @@ Article.all.filter(rating__gte: 10) ### `gt` -Allows to filter record based on field values that are greater than a specified value. +Allows filtering records based on field values that are greater than a specified value. ```crystal Article.all.filter(rating__gt: 10) @@ -460,7 +606,7 @@ Article.all.filter(rating__gt: 10) ### `icontains` -Allows to filter records based on field values that contain a specific substring, in a case-insensitive way. +Allows filtering records based on field values that contain a specific substring, in a case-insensitive way. ```crystal Article.all.filter(title__icontains: "tech") @@ -468,7 +614,7 @@ Article.all.filter(title__icontains: "tech") ### `iendswith` -Allows to filter records based on field values that end with a specific substring, in a case-insensitive way. +Allows filtering records based on field values that end with a specific substring, in a case-insensitive way. ```crystal Article.all.filter(title__iendswith: "travel") @@ -476,7 +622,7 @@ Article.all.filter(title__iendswith: "travel") ### `iexact` -Allows to filter records based on a specific field value (exact match), in a case-insensitive way. +Allows filtering records based on a specific field value (exact match), in a case-insensitive way. ```crystal Article.all.filter(title__iexact: "Top blog posts") @@ -484,7 +630,7 @@ Article.all.filter(title__iexact: "Top blog posts") ### `istartswith` -Allows to filter records based on field values that start with a specific substring, in a case-insensitive way. +Allows filtering records based on field values that start with a specific substring, in a case-insensitive way. ```crystal Article.all.filter(title__istartswith: "top") @@ -492,7 +638,7 @@ Article.all.filter(title__istartswith: "top") ### `in` -Allows to filter records based on field values that are contained in a specific array of values. +Allows filtering records based on field values that are contained in a specific array of values. ```crystal Tag.all.filter(slug__in=["foo", "bar", "xyz"]) @@ -500,7 +646,7 @@ Tag.all.filter(slug__in=["foo", "bar", "xyz"]) ### `isnull` -Allows to filter records based on field values that should be null or not null. +Allows filtering records based on field values that should be null or not null. ```crystal Article.all.filter(subtitle__isnull: true) @@ -509,7 +655,7 @@ Article.all.filter(subtitle__isnull: false) ### `lte` -Allows to filter record based on field values that are less than or equal to a specified value. +Allows filtering records based on field values that are less than or equal to a specified value. ```crystal Article.all.filter(rating__lte: 10) @@ -517,7 +663,7 @@ Article.all.filter(rating__lte: 10) ### `lt` -Allows to filter record based on field values that are less than a specified value. +Allows filtering records based on field values that are less than a specified value. ```crystal Article.all.filter(rating__lt: 10) @@ -525,7 +671,7 @@ Article.all.filter(rating__lt: 10) ### `startswith` -Allows to filter records based on field values that start with a specific substring. Note that this is a **case-sensitive** predicate. +Allows filtering records based on field values that start with a specific substring. Note that this is a **case-sensitive** predicate. ```crystal Article.all.filter(title__startswith: "Top") diff --git a/docs/versioned_docs/version-0.4/models-and-databases/reference/table-options.md b/docs/versioned_docs/version-0.4/models-and-databases/reference/table-options.md new file mode 100644 index 000000000..2c78c93ba --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.4/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.4/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.4/models-and-databases/relationships.md b/docs/versioned_docs/version-0.4/models-and-databases/relationships.md new file mode 100644 index 000000000..42bc175a6 --- /dev/null +++ b/docs/versioned_docs/version-0.4/models-and-databases/relationships.md @@ -0,0 +1,425 @@ +--- +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 # [#, + # #] +``` + +:::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.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, 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.1/models-and-databases/transactions.md b/docs/versioned_docs/version-0.4/models-and-databases/transactions.md similarity index 80% rename from docs/versioned_docs/version-0.1/models-and-databases/transactions.md rename to docs/versioned_docs/version-0.4/models-and-databases/transactions.md index 022658d40..2a292ed2a 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/transactions.md +++ b/docs/versioned_docs/version-0.4/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.1/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [model classes](pathname:///api/0.1/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.4/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [model classes](pathname:///api/0.4/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.1/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [a model class](pathname:///api/0.1/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.4/Marten/DB/Model/Connection.html#transaction(using%3ANil|String|Symbol%3Dnil%2C%26block)-instance-method) or [a model class](pathname:///api/0.4/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,11 +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). + ## 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.1/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.4/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.1/models-and-databases/validations.md b/docs/versioned_docs/version-0.4/models-and-databases/validations.md similarity index 99% rename from docs/versioned_docs/version-0.1/models-and-databases/validations.md rename to docs/versioned_docs/version-0.4/models-and-databases/validations.md index 6e5bace74..6d0cf0fc7 100644 --- a/docs/versioned_docs/version-0.1/models-and-databases/validations.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/Marten/Core/Validation/ErrorSet.html). ### Inspecting errors diff --git a/docs/versioned_docs/version-0.1/prologue.md b/docs/versioned_docs/version-0.4/prologue.mdx similarity index 71% rename from docs/versioned_docs/version-0.1/prologue.md rename to docs/versioned_docs/version-0.4/prologue.mdx index 639e0f9ac..e775289a3 100644 --- a/docs/versioned_docs/version-0.1/prologue.md +++ b/docs/versioned_docs/version-0.4/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.1/index.html) is also available to dig into Marten's internals. +Additionally, an automatically-generated [API reference](pathname:///api/0.4/index.html) is also available to dig into Marten's internals. diff --git a/docs/versioned_docs/version-0.1/schemas.mdx b/docs/versioned_docs/version-0.4/schemas.mdx similarity index 100% rename from docs/versioned_docs/version-0.1/schemas.mdx rename to docs/versioned_docs/version-0.4/schemas.mdx diff --git a/docs/versioned_docs/version-0.1/schemas/how-to/create-custom-schema-fields.md b/docs/versioned_docs/version-0.4/schemas/how-to/create-custom-schema-fields.md similarity index 97% rename from docs/versioned_docs/version-0.1/schemas/how-to/create-custom-schema-fields.md rename to docs/versioned_docs/version-0.4/schemas/how-to/create-custom-schema-fields.md index 78d8c7808..971cf524c 100644 --- a/docs/versioned_docs/version-0.1/schemas/how-to/create-custom-schema-fields.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/Marten/Schema/Field/Base.html) is fairly simply and looks like this: ```crystal def initialize( diff --git a/docs/versioned_docs/version-0.1/schemas/introduction.md b/docs/versioned_docs/version-0.4/schemas/introduction.md similarity index 69% rename from docs/versioned_docs/version-0.1/schemas/introduction.md rename to docs/versioned_docs/version-0.4/schemas/introduction.md index 822577d30..531ac5f5c 100644 --- a/docs/versioned_docs/version-0.1/schemas/introduction.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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,7 +64,7 @@ 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.4/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.4/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: @@ -191,10 +191,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.4/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) or [`#invalid?`](pathname:///api/0.4/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.4/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) or [`#invalid?`](pathname:///api/0.4/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.4/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.4/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.4/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 +257,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.4/Marten/Core/Validation.html#valid%3F(context%3ANil|String|Symbol%3Dnil)-instance-method) or [`#invalid?`](pathname:///api/0.4/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.4/schemas/reference/fields.md b/docs/versioned_docs/version-0.4/schemas/reference/fields.md new file mode 100644 index 000000000..5d671821d --- /dev/null +++ b/docs/versioned_docs/version-0.4/schemas/reference/fields.md @@ -0,0 +1,162 @@ +--- +title: Schema fields +description: Schema fields reference. +--- + +This page provides a reference for all the available field options and field types that can be used when defining schemas. + +## Common field options + +The following field options can be used for all the available field types when declaring schema fields using the `field` macro. + +### `required` + +The `required` argument can be used to specify whether a schema field is required or not required. The default value for this argument is `true`. + +## Field types + +### `bool` + +A `bool` field allows validating boolean values. + +### `date_time` + +A `date_time` field allows validating date time values. Fields using this type are converted to `Time` objects in Crystal. + +### `date` + +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: + +#### `max_size` + +The `max_size` argument allows defining the maximum size allowed for the email address string. The default value for this argument is `254` (in accordance with RFCs 3696 and 5321). + +#### `min_size` + +The `min_size` argument allows defining the minimum size allowed for the email address 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`. + +### `file` + +A `file` field allows validating uploaded files. In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `allow_empty_files` + +The `allow_empty_files` argument allows defining whether empty files are allowed or not when files are validated. The default value is `false`. + +#### `max_name_size` + +The `max_name_size` argument allows defining the maximum file name size allowed. The default value is `nil`, which means that uploaded file name sizes are not validated. + +### `float` + +A `float` field allows validating float values. Fields using this type are converted to `Float64` objects in Crystal. In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `max_value` + +The `max_value` argument allows defining the maximum value allowed. The default value for this argument is `nil`, which means that the maximum value is not validated by default. + +#### `min_value` + +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. + +### `int` + +An `int` field allows validating integer values. Fields using this type are converted to `Int64` objects in Crystal. In addition to the [common field options](#common-field-options), such fields support the following arguments: + +#### `max_value` + +The `max_value` argument allows defining the maximum value allowed. The default value for this argument is `nil`, which means that the maximum value is not validated by default. + +#### `min_value` + +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: + +#### `max_size` + +The `max_size` argument allows defining the maximum size allowed for the string. The default value for this argument is `nil`, which means that the maximum size is not validated by default. + +#### `min_size` + +The `min_size` argument allows defining the minimum size allowed for the 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`. + +### `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.1/schemas/validations.md b/docs/versioned_docs/version-0.4/schemas/validations.md similarity index 99% rename from docs/versioned_docs/version-0.1/schemas/validations.md rename to docs/versioned_docs/version-0.4/schemas/validations.md index fe483e7c7..3561d5c9a 100644 --- a/docs/versioned_docs/version-0.1/schemas/validations.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/Marten/Core/Validation/ErrorSet.html). ### Inspecting errors diff --git a/docs/versioned_docs/version-0.1/security.mdx b/docs/versioned_docs/version-0.4/security.mdx similarity index 79% rename from docs/versioned_docs/version-0.1/security.mdx rename to docs/versioned_docs/version-0.4/security.mdx index aadd5ed5b..d6dd7ef64 100644 --- a/docs/versioned_docs/version-0.1/security.mdx +++ b/docs/versioned_docs/version-0.4/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.1/security/clickjacking.md b/docs/versioned_docs/version-0.4/security/clickjacking.md similarity index 97% rename from docs/versioned_docs/version-0.1/security/clickjacking.md rename to docs/versioned_docs/version-0.4/security/clickjacking.md index bc0dd36f7..7e8a3934f 100644 --- a/docs/versioned_docs/version-0.1/security/clickjacking.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.4/security/content-security-policy.md b/docs/versioned_docs/version-0.4/security/content-security-policy.md new file mode 100644 index 000000000..7f6de311b --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/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.4/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.4/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.4/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.4/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.4/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.1/security/csrf.md b/docs/versioned_docs/version-0.4/security/csrf.md similarity index 97% rename from docs/versioned_docs/version-0.1/security/csrf.md rename to docs/versioned_docs/version-0.4/security/csrf.md index 25d0ddaab..6fd2e904f 100644 --- a/docs/versioned_docs/version-0.1/security/csrf.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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 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.1/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.4/Marten/Handlers/RequestForgeryProtection.html) module. ## Basic usage @@ -99,7 +99,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.1/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.4/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.1/security/introduction.md b/docs/versioned_docs/version-0.4/security/introduction.md similarity index 84% rename from docs/versioned_docs/version-0.1/security/introduction.md rename to docs/versioned_docs/version-0.4/security/introduction.md index fba2706ac..190e399c2 100644 --- a/docs/versioned_docs/version-0.1/security/introduction.md +++ b/docs/versioned_docs/version-0.4/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.4/static/img/getting-started/tutorial/marten_welcome_page.png b/docs/versioned_docs/version-0.4/static/img/getting-started/tutorial/marten_welcome_page.png new file mode 100644 index 000000000..28ab12ed5 Binary files /dev/null and b/docs/versioned_docs/version-0.4/static/img/getting-started/tutorial/marten_welcome_page.png differ diff --git a/docs/versioned_docs/version-0.4/static/img/prologue/logo.png b/docs/versioned_docs/version-0.4/static/img/prologue/logo.png new file mode 100644 index 000000000..d69865aee Binary files /dev/null and b/docs/versioned_docs/version-0.4/static/img/prologue/logo.png differ diff --git a/docs/versioned_docs/version-0.1/templates.mdx b/docs/versioned_docs/version-0.4/templates.mdx similarity index 100% rename from docs/versioned_docs/version-0.1/templates.mdx rename to docs/versioned_docs/version-0.4/templates.mdx diff --git a/docs/versioned_docs/version-0.1/templates/how-to/create-custom-context-producers.md b/docs/versioned_docs/version-0.4/templates/how-to/create-custom-context-producers.md similarity index 92% rename from docs/versioned_docs/version-0.1/templates/how-to/create-custom-context-producers.md rename to docs/versioned_docs/version-0.4/templates/how-to/create-custom-context-producers.md index d18cbb6e1..779c3a6fd 100644 --- a/docs/versioned_docs/version-0.1/templates/how-to/create-custom-context-producers.md +++ b/docs/versioned_docs/version-0.4/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.1/Marten/Template/ContextProducer.html) abstract class. This abstract class requires that subclasses implement a single [`#produce`](pathname:///api/0.1/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.4/Marten/Template/ContextProducer.html) abstract class. This abstract class requires that subclasses implement a single [`#produce`](pathname:///api/0.4/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 diff --git a/docs/versioned_docs/version-0.1/templates/how-to/create-custom-filters.md b/docs/versioned_docs/version-0.4/templates/how-to/create-custom-filters.md similarity index 90% rename from docs/versioned_docs/version-0.1/templates/how-to/create-custom-filters.md rename to docs/versioned_docs/version-0.4/templates/how-to/create-custom-filters.md index 8e37dfd5c..d8c49ccc0 100644 --- a/docs/versioned_docs/version-0.1/templates/how-to/create-custom-filters.md +++ b/docs/versioned_docs/version-0.4/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.1/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.1/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.4/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.4/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.1/Marten/Template/Value.html) object. +As you can see, the `#apply` method must return a [`Marten::Template::Value`](pathname:///api/0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.1/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.4/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.4/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.1/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.4/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.1/templates/how-to/create-custom-tags.md b/docs/versioned_docs/version-0.4/templates/how-to/create-custom-tags.md similarity index 94% rename from docs/versioned_docs/version-0.1/templates/how-to/create-custom-tags.md rename to docs/versioned_docs/version-0.4/templates/how-to/create-custom-tags.md index a58ab8a60..2cafc1e12 100644 --- a/docs/versioned_docs/version-0.1/templates/how-to/create-custom-tags.md +++ b/docs/versioned_docs/version-0.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.4/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.1/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.1/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.1/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.4/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.4/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.4/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.1/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.4/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.1/templates/introduction.md b/docs/versioned_docs/version-0.4/templates/introduction.md similarity index 90% rename from docs/versioned_docs/version-0.1/templates/introduction.md rename to docs/versioned_docs/version-0.4/templates/introduction.md index 83caa7688..d046b700f 100644 --- a/docs/versioned_docs/version-0.1/templates/introduction.md +++ b/docs/versioned_docs/version-0.4/templates/introduction.md @@ -46,6 +46,12 @@ Each variable can involve additional lookups in order to access specific object

{{ article.title }}

``` +This notation can be used to call object methods but also to perform key lookups for hashes or named tuples. It can also be used to perform index lookups for indexable objects (such as arrays or tuples): + +``` +{{ my_array.0 }} +``` + ### Filters Filters can be applied to [variables](#variables) or [tag](#tags) arguments in order to transform their values. They are applied to these variables or arguments through the use of a pipe (**`|`**) followed by the name of the filter. @@ -181,19 +187,19 @@ Templates can be loaded from specific locations within your codebase and from ap 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.1/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.4/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.1/Marten/Template/Template.html) object that you can then render by using a specific context. +This will return a compiled [`Template`](pathname:///api/0.4/Marten/Template/Template.html) object that you can then render by using a specific context. ## 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.1/Marten/Template/Template.html) object that you loaded by leveraging the [`#render`](pathname:///api/0.1/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.4/Marten/Template/Template.html) object that you loaded by leveraging the [`#render`](pathname:///api/0.4/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") @@ -204,10 +210,10 @@ template.render({ foo: "bar" }) ## 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.1/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.4/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.1/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.4/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: @@ -234,7 +240,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.1/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.4/Marten/Template/Object.html) module in the `Point` class and define a `#resolve_template_attribute` method as follows: ```crystal class Point @@ -257,9 +263,9 @@ class Point end ``` -Each class including the [`Marten::Template::Object`](pathname:///api/0.1/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.4/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.1/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.4/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 @@ -275,7 +281,7 @@ class Point end ``` -Another possibility is to include the [`Marten::Template::Object::Auto`](pathname:///api/0.1/Marten/Template/Object/Auto.html) module instead of the [`Marten::Template::Object`](pathname:///api/0.1/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.4/Marten/Template/Object/Auto.html) module instead of the [`Marten::Template::Object`](pathname:///api/0.4/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 @@ -289,13 +295,13 @@ 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.1/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.1/Marten/Template/Object.html) and the [`#template_attributes`](pathname:///api/0.1/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.4/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.4/Marten/Template/Object.html) and the [`#template_attributes`](pathname:///api/0.4/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.1/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.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: @@ -345,3 +351,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.4/Marten/Template/Errors/UnknownVariable.html) exceptions. diff --git a/docs/versioned_docs/version-0.1/templates/reference/context-producers.md b/docs/versioned_docs/version-0.4/templates/reference/context-producers.md similarity index 87% rename from docs/versioned_docs/version-0.1/templates/reference/context-producers.md rename to docs/versioned_docs/version-0.4/templates/reference/context-producers.md index ef749944a..8e2ef8e09 100644 --- a/docs/versioned_docs/version-0.1/templates/reference/context-producers.md +++ b/docs/versioned_docs/version-0.4/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.1/Marten/Template/ContextProducer/Debug.html) +**Class:** [`Marten::Template::ContextProducer::Debug`](pathname:///api/0.4/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.1/Marten/Template/ContextProducer/Flash.html) +**Class:** [`Marten::Template::ContextProducer::Flash`](pathname:///api/0.4/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.1/Marten/Template/ContextProducer/I18n.html) +**Class:** [`Marten::Template::ContextProducer::I18n`](pathname:///api/0.4/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.1/Marten/Template/ContextProducer/Request.html) +**Class:** [`Marten::Template::ContextProducer::Request`](pathname:///api/0.4/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.4/templates/reference/filters.md b/docs/versioned_docs/version-0.4/templates/reference/filters.md new file mode 100644 index 000000000..d95474751 --- /dev/null +++ b/docs/versioned_docs/version-0.4/templates/reference/filters.md @@ -0,0 +1,134 @@ +--- +title: Template filters +description: Template filters reference. +--- + +This page provides a reference for all the available filters that can be used when defining [templates](../introduction.md). + +## `capitalize` + +The `capitalize` filter allows to modify a string so that the first letter is converted to uppercase and all the subsequent letters are converted to lowercase. + +For example: + +```html +{{ value|capitalize }} +``` + +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 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: + +```html +{{ value|default:"foobar" }} +``` + +If `value` is `nil` (or `0` or `false`), the output will be "foobar". + +## `downcase` + +The `downcase` filter allows to convert a string so that each of its characters is lowercase. + +For example: + +```html +{{ value|downcase }} +``` + +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 (`
`). + +For example: + +```html +{{ value|linebreaks }} +``` + +If `value` is `Hello\nWorld`, then the output will be `Hello
World`. + +## `safe` + +The `safe` filter allows to mark that a string is safe and that it should not be escaped before being inserted in the final output of a rendered template. Indeed, string values are always automatically HTML-escaped by default in templates. + +For example: + +```html +{{ value|safe }} +``` + +If `value` is `

Hello

`, then the output will be `

Hello

` as well. + +## `size` + +The `size` filter allows returning the size of a string or an enumerable object. + +For example: + +```html +{{ value|size }} +``` + +## `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` + +The `upcase` filter allows to convert a string so that each of its characters is uppercase. + +For example: + +```html +{{ value|upcase }} +``` + +If `value` is "Hello", then the output will be "HELLO". diff --git a/docs/versioned_docs/version-0.1/templates/reference/tags.md b/docs/versioned_docs/version-0.4/templates/reference/tags.md similarity index 86% rename from docs/versioned_docs/version-0.1/templates/reference/tags.md rename to docs/versioned_docs/version-0.4/templates/reference/tags.md index 83181cdd3..457be9357 100644 --- a/docs/versioned_docs/version-0.1/templates/reference/tags.md +++ b/docs/versioned_docs/version-0.4/templates/reference/tags.md @@ -5,7 +5,7 @@ description: Template tags reference. ## `asset` -The `asset` template tag allows to generate the URL of a given [asset](../../files/asset-handling). It must take at least one argument (the filepath of the asset). +The `asset` template tag allows to generate the URL of a given [asset](../../assets/introduction.md). It must take at least one argument (the filepath of the asset). For example, the following line is a valid usage of the `asset` tag and will output the path or URL of the `app/app.css` asset: @@ -33,6 +33,26 @@ For example: 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 %} +``` + ## `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). @@ -83,8 +103,8 @@ Finally, loops give access to a special `loop` variable _inside_ the loop in ord | `loop.index0` | The index of the current iteration (0-indexed) | | `loop.revindex` | The index of the current iteration counting from the end of the loop (1-indexed) | | `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.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.parent` | The parent's `loop` variable (only for nested for loops) | ## `if` @@ -237,3 +257,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.1/the-marten-project.mdx b/docs/versioned_docs/version-0.4/the-marten-project.mdx similarity index 79% rename from docs/versioned_docs/version-0.1/the-marten-project.mdx rename to docs/versioned_docs/version-0.4/the-marten-project.mdx index 0b93daa5b..383cf00ee 100644 --- a/docs/versioned_docs/version-0.1/the-marten-project.mdx +++ b/docs/versioned_docs/version-0.4/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.1/the-marten-project/acknowledgments.md b/docs/versioned_docs/version-0.4/the-marten-project/acknowledgments.md similarity index 89% rename from docs/versioned_docs/version-0.1/the-marten-project/acknowledgments.md rename to docs/versioned_docs/version-0.4/the-marten-project/acknowledgments.md index 5ddd5dddc..9429027da 100644 --- a/docs/versioned_docs/version-0.1/the-marten-project/acknowledgments.md +++ b/docs/versioned_docs/version-0.4/the-marten-project/acknowledgments.md @@ -27,12 +27,13 @@ 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.1/Marten/Core/Encryptor.html) and [message signers](pathname:///api/0.1/Marten/Core/Signer.html) +* The idea of [message encryptors](pathname:///api/0.4/Marten/Core/Encryptor.html) and [message signers](pathname:///api/0.4/Marten/Core/Signer.html) ### But also... * The exception page displayed while in [debug](../development/reference/settings.md#debug) mode is inspired by the [Exception Page](https://github.com/crystal-loot/exception_page) shard * The way to [handle custom objects](../templates/introduction.md#using-custom-objects-in-contexts) in Marten templates is inspired by a similar mechanism within [Crinja](https://github.com/straight-shoota/crinja) +* The idea of class-based [email definitions](../emailing/introduction.md) is borrowed from [Carbon](https://github.com/luckyframework/carbon) ## Contributors diff --git a/docs/versioned_docs/version-0.1/the-marten-project/contributing.md b/docs/versioned_docs/version-0.4/the-marten-project/contributing.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/contributing.md rename to docs/versioned_docs/version-0.4/the-marten-project/contributing.md diff --git a/docs/versioned_docs/version-0.4/the-marten-project/design-philosophies.md b/docs/versioned_docs/version-0.4/the-marten-project/design-philosophies.md new file mode 100644 index 000000000..0ad1da8ad --- /dev/null +++ b/docs/versioned_docs/version-0.4/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 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.4/the-marten-project/release-notes.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes.md new file mode 100644 index 000000000..ff2c7464e --- /dev/null +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes.md @@ -0,0 +1,36 @@ +--- +title: Release notes +description: Find all the release notes of the Marten web framework. +pagination_next: null +--- + +Here are listed the release notes for each version of the Marten web framework. + +## Marten 0.4 + +* [Marten 0.4 release notes](./release-notes/0.4.md) _(under development)_ + +## 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) +* [Marten 0.2.3 release notes](./release-notes/0.2.3.md) +* [Marten 0.2.2 release notes](./release-notes/0.2.2.md) +* [Marten 0.2.1 release notes](./release-notes/0.2.1.md) +* [Marten 0.2 release notes](./release-notes/0.2.md) + +## Marten 0.1 + +* [Marten 0.1.5 release notes](./release-notes/0.1.5.md) +* [Marten 0.1.4 release notes](./release-notes/0.1.4.md) +* [Marten 0.1.3 release notes](./release-notes/0.1.3.md) +* [Marten 0.1.2 release notes](./release-notes/0.1.2.md) +* [Marten 0.1.1 release notes](./release-notes/0.1.1.md) +* [Marten 0.1 release notes](./release-notes/0.1.md) diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.1.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.1.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.1.md rename to docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.1.md diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.2.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.2.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.2.md rename to docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.2.md diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.3.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.3.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.3.md rename to docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.3.md diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.4.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.4.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.4.md rename to docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.4.md diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.5.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.5.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.5.md rename to docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.5.md diff --git a/docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.md similarity index 100% rename from docs/versioned_docs/version-0.1/the-marten-project/release-notes/0.1.md rename to docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.1.md diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.1.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.1.md new file mode 100644 index 000000000..67be5b3f8 --- /dev/null +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.1.md @@ -0,0 +1,13 @@ +--- +title: Marten 0.2.1 release notes +pagination_prev: null +pagination_next: null +--- + +_February 25, 2023._ + +## Bug fixes + +* Fix possible empty field names in messages associated with exceptions raised when targetting non-existing fields with query sets +* Fix broken methods inherited from the [`Enumerable`](https://crystal-lang.org/api/1.6.2/Enumerable.html) mixin for request parameter abstractions +* Fix a possible typing issue related to the use of `Marten::Emailing::Backend::Development#delivered_emails` in specs generated for projects with authentication diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.2.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.2.md new file mode 100644 index 000000000..1410d2d43 --- /dev/null +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.2.md @@ -0,0 +1,13 @@ +--- +title: Marten 0.2.2 release notes +pagination_prev: null +pagination_next: null +--- + +_March 7, 2023._ + +## Bug fixes + +* Fix possible compilation errors happening when inheriting from a schema containing fields with options +* Fix an issue where model field validations were inconsistent with validation callbacks (eg. model field validations were called before `before_validation` callbacks) +* Ensure that exception messages and snippet lines are properly escaped in the server error debug page diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.3.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.3.md new file mode 100644 index 000000000..f2bc9ac31 --- /dev/null +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.3.md @@ -0,0 +1,12 @@ +--- +title: Marten 0.2.3 release notes +pagination_prev: null +pagination_next: null +--- + +_March 23, 2023._ + +## Bug fixes + +* Fix possible incorrect migration dependency generation when two separate apps have migrations generated at the same time +* Fix possible incorrect circular dependency error raised when verifying the acyclic property of the migrations graph diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.4.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.4.md new file mode 100644 index 000000000..258c8b039 --- /dev/null +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.4.md @@ -0,0 +1,12 @@ +--- +title: Marten 0.2.4 release notes +pagination_prev: null +pagination_next: null +--- + +_May 15, 2023._ + +## Bug fixes + +* Fix possible `KeyError` exception raised when the [use_x_forwarded_proto](../../development/reference/settings.md#use_x_forwarded_proto) setting is set to `true`. +* Fix overly strict route name rules. 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 new file mode 100644 index 000000000..039c9b614 --- /dev/null +++ b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.2.md @@ -0,0 +1,133 @@ +--- +title: Marten 0.2.0 release notes +pagination_prev: null +pagination_next: null +--- + +_February 11, 2023._ + +## Requirements and compatibility + +Crystal 1.6 and 1.7. + +## New features + +### Authentication + +The framework now provides the ability to generate projects with a [built-in authentication system](../../authentication.mdx) that handles basic user management needs: signing in/out users, resetting passwords, etc. This can be achieved through the use of the `--with-auth` option of the [`new`](../../development/reference/management-commands.md#new) management command: + +```bash +marten new project myblog --with-auth +``` + +The `--with-auth` option ensures that an `auth` [application](../../development/applications.md) is created for the generated project under the `src/auth` folder. This application is part of the created project and provides the necessary [models](../../models-and-databases.mdx), [handlers](../../handlers-and-http.mdx), [schemas](../../schemas.mdx), [emails](../../emailing.mdx), and [templates](../../templates.mdx) allowing authenticating users with email addresses and passwords, while also supporting standard password reset flows. All these abstractions provide a "default" authentication implementation that can be then updated on a per-project basis to better accommodate authentication-related requirements. + +### Email sending + +Marten now lets you define [emails](../../emailing.mdx) that you can fully customize (properties, header values, etc) and whose bodies (HTML and/or text) are rendered by leveraging [templates](../../templates.mdx). For example, here is how to define a simple email and how to deliver it: + +```crystal +class WelcomeEmail < Marten::Email + to @user.email + subject "Hello!" + template_name "emails/welcome_email.html" + + def initialize(@user : User) + end +end + +email = WelcomeEmail.new(user) +email.deliver +``` + +Emails are delivered by leveraging an [emailing backend mechanism](../../emailing/introduction.md#emailing-backends). Emailing backends implement _how_ emails are actually sent and delivered. Presently, Marten supports one built-in [development emailing backend](../../emailing/reference/backends.md#development-backend), and a set of other [third-party backends](../../emailing/reference/backends.md#other-backends) that you can install depending on your email sending requirements. + +Please refer to the [Emailing section](../../emailing.mdx) to learn more about this new feature. + +### Raw SQL capabilities + +Query sets now provide the ability to perform raw queries that are mapped to actual model instances. This is interesting if the capabilities provided by query sets are not sufficient for the task at hand and you need to write custom SQL queries. + +For example: + +```crystal +Article.raw("SELECT * FROM articles WHERE title = ?", "Hello World!").each do |article| + # Do something with `article` record +end +``` + +Please refer to [Raw SQL](../../models-and-databases/raw-sql.md) to learn more about this capability. + +### Email field for models and schemas + +It is now possible to define `email` fields in [models](../../models-and-databases/reference/fields.md#email) and [schemas](../../schemas/reference/fields.md#email). These allow you to easily persist valid email addresses in your models but also to expect valid email addresses 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 :email, :email, unique: true +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. + +For example: + +```crystal +class User < Marten::Model + field :id, :big_int, primary_key: true, auto: true + field :username, :string, max_size: 64, unique: true + + after_commit :do_something, on: :update + + private def do_something + # Do something! + end +end +``` + +Please refer to [Callbacks](../../models-and-databases/callbacks.md) to learn more about this capability. + +### Minor features + +#### Models and databases + +* Support for [DB connection pool parameters](https://crystal-lang.org/reference/database/connection_pool.html) was added. See the [database settings reference](../../development/reference/settings.md#database-settings) for more details about the supported parameters +* Model fields now contribute `#?` methods to model classes in order to easily identify if a field has a value or not. Note that this capability is also enabled for the relationship methods provided by the [`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 +* It is now possible to leverage a [`#with_timestamp_fields`](pathname:///api/0.2/Marten/DB/Model/Table.html#with_timestamp_fields-macro) macro to automatically create `created_at` / `updated_at` timestamp fields in models. 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. See [Timestamps](../../models-and-databases/introduction.md#timestamps) to learn more about this capability +* It is now possible to easily retrieve specific column values without loading entire record objects by leveraging the [`#pluck`](../../models-and-databases/reference/query-set.md#pluck) and [`#pick`](../../models-and-databases/reference/query-set.md#pick) query set methods + +#### Handlers and HTTP + +* `string` can now be used as an alias for `str` when defining [routing parameters](../../handlers-and-http/routing.md#specifying-route-parameters) + +#### Templates + +* Support for index lookups was added. This means that it is now possible to retrieve specific index values from indexable objects in templates using the `{{ var. }}` syntax (for example: `{{ var.0 }}`) +* A [`linebreaks`](../../templates/reference/filters.md#linebreaks) template filter was introduced to allow replacing all newlines with HTML line breaks (`
`) in strings easily + +#### Schemas + +* Schemas can now be initialized from handler routing parameters (which means that they can be initialized from the hashes that are returned by the [`Marten::Handlers::Base#params`](pathname:///api/0.2/Marten/Handlers/Base.html#params%3AHash(String%2CInt16|Int32|Int64|Int8|String|UInt16|UInt32|UInt64|UInt8|UUID)-instance-method) method) + +#### Management commands + +* The [`serve`](../../development/reference/management-commands.md#serve) management command now supports the ability to override the host and port used by the development server + +#### Testing + +* A [testing client](../../development/testing.md#using-the-test-client) was introduced in order to allow developers to easily test handlers (and the various routes of a project) by issuing requests and by introspecting the returned responses. + +## Backward incompatible changes + +### Templates + +* The `loop.first` and `loop.last` for loop variables were respectively renamed `loop.first?` and `loop.last?`. See the [template tag reference](../../templates/reference/tags.md#for) to learn more about the `for` template tag + +### Management commands + +* The [`new`](../../development/reference/management-commands.md#new) management command's optional directory argument was replaced by a dedicated `-d DIR, --dir=DIR` option diff --git a/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.1.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.1.md new file mode 100644 index 000000000..f53946252 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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/dev/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.4/the-marten-project/release-notes/0.3.2.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.2.md new file mode 100644 index 000000000..5feb3f9b2 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/the-marten-project/release-notes/0.3.3.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.3.md new file mode 100644 index 000000000..b260e325c --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/the-marten-project/release-notes/0.3.4.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.4.md new file mode 100644 index 000000000..3a5fcfe5f --- /dev/null +++ b/docs/versioned_docs/version-0.4/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.4/the-marten-project/release-notes/0.3.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.3.md new file mode 100644 index 000000000..7bedfef92 --- /dev/null +++ b/docs/versioned_docs/version-0.4/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#allowedhosts) 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.4/the-marten-project/release-notes/0.4.md b/docs/versioned_docs/version-0.4/the-marten-project/release-notes/0.4.md new file mode 100644 index 000000000..bf79ecfbc --- /dev/null +++ b/docs/versioned_docs/version-0.4/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 +--- + +_Under development._ + +## 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`](#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_sidebars/version-0.1-sidebars.json b/docs/versioned_sidebars/version-0.4-sidebars.json similarity index 64% rename from docs/versioned_sidebars/version-0.1-sidebars.json rename to docs/versioned_sidebars/version-0.4-sidebars.json index 8ec444bba..050025cad 100644 --- a/docs/versioned_sidebars/version-0.1-sidebars.json +++ b/docs/versioned_sidebars/version-0.4-sidebars.json @@ -23,10 +23,12 @@ "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", "models-and-databases/transactions", + "models-and-databases/raw-sql", "models-and-databases/multiple-databases", { "type": "category", @@ -40,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" ] @@ -58,8 +61,10 @@ "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", @@ -132,6 +137,17 @@ } ] }, + { + "type": "category", + "label": "Assets", + "link": { + "type": "doc", + "id": "assets" + }, + "items": [ + "assets/introduction" + ] + }, { "type": "category", "label": "Files", @@ -142,7 +158,13 @@ "items": [ "files/uploading-files", "files/managing-files", - "files/asset-handling" + { + "type": "category", + "label": "How-To's", + "items": [ + "files/how-to/create-custom-file-storages" + ] + } ] }, { @@ -156,11 +178,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" ] }, @@ -169,7 +193,8 @@ "label": "Reference", "items": [ "development/reference/settings", - "development/reference/management-commands" + "development/reference/management-commands", + "development/reference/generators" ] } ] @@ -184,7 +209,8 @@ "items": [ "security/introduction", "security/csrf", - "security/clickjacking" + "security/clickjacking", + "security/content-security-policy" ] }, { @@ -198,6 +224,75 @@ "i18n/introduction" ] }, + { + "type": "category", + "label": "Emailing", + "link": { + "type": "doc", + "id": "emailing" + }, + "items": [ + "emailing/introduction", + "emailing/callbacks", + { + "type": "category", + "label": "How-To's", + "items": [ + "emailing/how-to/create-custom-emailing-backends" + ] + }, + { + "type": "category", + "label": "Reference", + "items": [ + "emailing/reference/backends" + ] + } + ] + }, + { + "type": "category", + "label": "Authentication", + "link": { + "type": "doc", + "id": "authentication" + }, + "items": [ + "authentication/introduction", + { + "type": "category", + "label": "Reference", + "items": [ + "authentication/reference/generated-files" + ] + } + ] + }, + { + "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", @@ -211,7 +306,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" ] } ] @@ -225,6 +322,7 @@ }, "items": [ "the-marten-project/contributing", + "the-marten-project/design-philosophies", "the-marten-project/acknowledgments", { "type": "category", @@ -241,7 +339,18 @@ "the-marten-project/release-notes/0.1.2", "the-marten-project/release-notes/0.1.3", "the-marten-project/release-notes/0.1.4", - "the-marten-project/release-notes/0.1.5" + "the-marten-project/release-notes/0.1.5", + "the-marten-project/release-notes/0.2", + "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.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" ] } ] diff --git a/docs/versions.json b/docs/versions.json index 840caf92f..28959c222 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,5 +1,5 @@ [ + "0.4", "0.3", - "0.2", - "0.1" + "0.2" ] diff --git a/shard.yml b/shard.yml index 6616de772..551b3ed06 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: marten -version: 0.3.4 +version: 0.4.0 authors: - Morgan Aubert diff --git a/src/marten.cr b/src/marten.cr index 3aa58c94e..7078ab50f 100644 --- a/src/marten.cr +++ b/src/marten.cr @@ -40,7 +40,7 @@ require "./marten/server/**" require "./marten/template/**" module Marten - VERSION = "0.3.4" + VERSION = "0.4.0" Log = ::Log.for("marten")