Routex Extensions

View Source

List of extensions

A list of included extensions can be found in the README.

Routex Extensions extend the functionality provided by Routex to transform routes or generate new route based helper functions. Each extension is a module which implements the Routex.Extension behaviour.

Routex will call those callbacks at different stages before Routex handsoff the list with routes to Phoenix.Router for compilation.

Each extension provides a single feature and should minimize hard dependencies on other extensions. Instead, Routex advises to make use of the Routex.Attrs system to share attributes; allowing extensions to work together without being coupled.

The documentation of each extension lists any provided or required Routex.Attrs.

Callbacks and stages

Stage 1: Configure

This stage enables extensions to preprocess backend options upfront.

The configure/2 callback is called with the options provided to Routex.Backend and the name of the Routex backend. It is expected to return a new list of options.

Routex collects all options in this stage for subsequent stages. Therefore, extensions should add any fallback/default they might use themselves to the options in this stage.

To aid in code completion, the final configuration is passed as a struct to subsequent stages.

Stage 2: Transform

This stage is meant to change the properties of routes, which are at that moment Phoenix.Router.Route structs. The routes are grouped by Routex backend and processed per group, allowing an extension to use accumulating values within one iteration.

The transform/3 callback is called with a list of routes belonging to a Routex backend, the name of the backend and the current environment. It is expected to return a list of Phoenix.Router.Route structs.

Flattening option values

Extensions can make use of Routex.Attrs provided by Routex itself, Routex backends and other extensions.

To make the availability of the attributes as predictable as possible, Routex uses a flat structure which is stored in a routes' private.routex field. However, using a flat structure might conflict with developer experience; sometimes a nested structure to provide configuration options might be more suitable.

One responsibility of the transform/3 callback is to flatten the structure of attributes they use for each route they receive, so other extensions can use attributes set by the current extension without knowledge of the configuration structure.

Example The Alternatives extension uses nested options and allows inheritance of attributes from parent branches.

alternatives: %{
  "/" =>
    helper: nil,
    locale: "en_GB",
    branches: %{
      "nl" => %{
          helper: "nl",
          locale: "nl_NL"
        },
      "gb" => %{
        helper: "gb",
        }
    }
}

The Alternatives module is therefor responsible for flattening those for (itself and) other extensions to use. To take the route responsible for the "gb" branch as an example, the extension should add flattened attributes in the Route struct. It can do so using the helper function Routex.Attrs.put/2.

Routex.Attrs.put(route, [locale: "en_GB", helper: "gb"])

Now the Translation extension can search for the option :locale in the route's attributes, unaware of how that locale was initially configured.

Stage 3: Post Transform

The post_transform stage can be used knowing all other attributes of a route are available and no path will be transformed any further.

Stage 4: Create helper functions

In this stage helper functions can be generated which will be added to MyAppWeb.Router.RoutexHelpers.

The create_helpers/3 callback is called with a list of routes belonging to a Routex backend, the name of the Routex backend and the current environment. It is expected to return Elixir AST.

As a result the developer only has to import MyAppWeb.Router.RoutexHelpers for all helpers generated by extensions to be included in the app.

Guidelines

  • make functions not defined by the Routex.Extension behaviour private.
  • provide as many options and Routex.Attrs as possible; other extensions might use the information.
  • provide additional options and Routex.Attrs as flat list(s) so other extensions don't have to guess structure.
  • as other extensions might use options set by your extension, try to preserve any previously defined option or Routex.Attrs in future development

Important information about creating helpers

In Elixir, it's common practice to define multiple function clauses to handle different routes or patterns. However, during compilation, the Elixir compiler (leveraging the underlying Erlang compiler) transforms these multiple function clauses into a single function that uses a case expression for pattern matching. The key issue here isn't the efficiency of a case statement at runtime; rather, it's the transformation process itself. When you have hundreds of function clauses, the compiler must merge them into one case expression, which can lead to significant compile-time overhead.

To mitigate this, consider defining a single function clause that directly contains an explicit case expression. By doing so, you avoid triggering the compiler’s internal transformation process on a large number of separate clauses, thereby reducing the compile-time cost while keeping your runtime behavior clear and efficient.

Bad

def my_generated_helper("a"), do: "A"
def my_generated_helper("b"), do: "B"
def my_generated_helper("c"), do: "C"

Good

def my_generated_helper(letter) do
  case letter do
    "a" -> "A"
    "b" -> "B"
    "c" -> "C"
  end
end

Documentation

@moduledoc """
Summary of feature provided.

## Options
- `name` - description

## Example configuration
```diff
# file lib/example_web/routex_backend.ex
defmodule ExampleWeb.RoutexBackend do
  use Routex.Backend,
  extensions: [
+   Routex.Extension.Name
    Routex.Extension.Attrs
],
+ name_config: [name_opt: "value"]
```

## Usage example
```elixir
# file lib/example_web/template.ex
transform_template("/products/:id/edit")
```

## Pseudo result
```
/products/:id/edit  ⇒ /products/:id/edit
```

## `Routex.Attrs`
**Requires**
- none

**Sets**
- none

## Helpers
function_name(arg1 :: type) :: type
"""