View Source mix scribe.gen.html (Elixir Scribe v0.3.0)

Generates a module per action for a resource within a domain at lib/my_app_web/* and lib/my_app/*, with the related tests. This includes the controller, view, templates, the schema, the module for resource action with functions around an Ecto schema and the Resource API boundary module.

The goal and motivation is to encourage developers to write cleaner code in a more organized folder structure, enabling them to know in seconds all domains, resources, and actions used in a project. This also contributes to reduce the technical debt via less complexity and clear boundaries between domains, resources and actions.

Usage Examples

The scribe.gen.html generator will generate files in both lib/my_app_web/*, lib/my_app/* and tests/* folders with the same hierarchy structure.

For example, to create a fictitious online shop app you may start with this command:

mix scribe.gen.html Catalog Category categories name:string desc:string

The first argument is the domain name followed by the resource name and its plural name, which is also used as the schema table name. The second argument, Catalog, is the resource's schema. A schema is an Elixir module responsible for mapping database fields into an Elixir struct. The Catalog schema above specifies two fields with their respective colon-delimited data types: name:string and desc:string. See mix phx.gen.schema for more information on attributes.

By default the command will generate some default actions for each resource on a domain: ["list", "new", "read", "edit", "create", "update", "delete"].

We can also pass as many custom actions as we want with the flag --actions:

mix scribe.gen.html Catalog Product products name:string desc:string --actions import,export

When don't need to use the default actions, then just pass the flag --no-default-actions:

mix scribe.gen.html Warehouse Stock stocks product_id:integer quantity:integer --actions import,export --no-default-actions

The folder structure

By using the previous command examples we get this folder structure:

$ tree -d -L 4 lib/my_app_web

lib/my_app_web
├── components
│   └── layouts
├── controllers
│   └── page_html
└── domains
    ├── catalog
    │   ├── category
    │   │   ├── create
    │   │   ├── delete
    │   │   ├── edit
    │   │   ├── export
    │   │   ├── import
    │   │   ├── list
    │   │   ├── new
    │   │   ├── read
    │   │   └── update
    │   └── product
    │       ├── create
    │       ├── delete
    │       ├── edit
    │       ├── export
    │       ├── import
    │       ├── list
    │       ├── new
    │       ├── read
    │       └── update
    └── warehouse
        └── stock
            ├── edit
            ├── export
            ├── import
            ├── list
            ├── new
            └── read

This generator adds the following files to lib/:

  • a controller per resource action at lib/my_app_web/domains/domain/resource/action.
  • default HTML templates per resource action in lib/my_app_web/domains/domain/resource/action.
  • an HTML view per resource in lib/my_app_web/domains/domain/resource.
  • a schema per resource in lib/my_app/domains/domain/resource.
  • a module per resource action in lib/my_app/domains/domain/resource/action
  • a resource API boundary module in lib/my_app/domains/domain.

Additionally, this generator creates the following files:

  • a migration for the schema in priv/repo/migrations.
  • a controller test module per resource action in test/my_app_web/domains/domain/resource/action.
  • a a module per resource action in test/my_app/domains/domain/resource/action.
  • a context test helper module in test/support/fixtures/[resource_name]_fixtures.ex.

The API Boundary

To prevent direct access between resources of the same domain or cross domains an API is provided for each Resource to act as a boundary that MUST never be crossed to not couple domains and resources via the internal implementation.

This enables developers to change the internal implementation of any action of a Resource knowing that the only caller is the resource API boundary.

The API boundary for each resource in a domain is located at the root of the domain folder. For example: MyApp.Catalog.CategoryAPI, which can be found at lib/my_app/catalog/category_api.ex.

Developers need to treat anything inside a resource as private, which for the Catalog domain (exemplified above) means to not access any module inside lib/my_app/catalog/category/*.

The Schema

The schema is responsible for mapping the database fields into an Elixir struct. A migration file for the repository will also be generated.

The schema can be found at lib/my_app/catalog/category_schema.ex and it's considered private, except for it's struct representation. To work with changesets you want to use the Domain API, and only used this module directly to pattern match on it's struct. For example, instead of using Category.changeset/2 to create a new changeset, use "CategoryAPI.new/1".

Generating without a schema

In some cases, you may wish to bootstrap the domain and its resources without any logic to access the schema, leaving the internal implementation to yourself. Use the --no-schema flag to accomplish this.

For example:

mix scribe.gen.html Accounts Company companies --no-schema

The Database Table

By default, the table name for the migration and schema will be the plural name provided for the resource. To customize this value, a --table option may be provided.

For example:

mix scribe.gen.html Accounts User users --table cms_users

Binary ID by Default

By default the generated migration uses binary_id for schema's primary key and its references. No toggle is provided to use numeric IDs, because they are the first vulnerability listed in OWASP API TOP 10 2023.

Security Implications of using numeric IDs

Numeric IDs allows an attacker to easily enumerate all resources by incrementing or decrementing the id by one, when proper access controls mechanisms aren't working as expected or may even be absent. This is the top one vulnerability in OWASP API TOP 10 2023, named as BOLA (Broken Object Level Authorization). Don't assume this will never happen in your code base.

Default options

This generator uses default options provided in the :generators configuration of your application in the same way phx.gen.html does, except for binary-id, which is ignored and always set to true by scribe.gen.domain. This is a design choice as per previous section on the use of Binary ID by Default.

Read the documentation for phx.gen.html and phx.gen.schema for more information on attributes.