Skip to main content

Rell modules

The Rell dapp consists of modules. A module is either a single .rell file or a directory with one or multiple .rell files.

A single-file Rell module must have a module header:

module;

// entities, operations, queries, functions, and other definitions

If a .rell file has no module header, it's part of a directory module. All such .rell files in a directory belong to the same directory module. An exception is a file called module.rell: it always belongs to a directory-module, even if it has a module header. A directory-module doesn't need to have module.rell.

Every file of a directory module sees definitions of all other module files. A file module file sees only its definitions. There may be a root module - a directory module that consists of .rell files located in the root of the source directory. The root module has an empty name.

Example of a Rell source directory tree:

.
└── app
├── module.rell
├── entities.rell
├── operations.rell
├── queries.rell
├── functions.rell
└── structs.rell

Understanding the file structure in depth reveals the organization and purpose behind each component, providing insights into how the module is built and operates:

  • module_name: This directory serves as the central hub for each module, housing all related files. It is the foundation where the module’s functionalities and features are defined and interconnected.

  • module.rell: This file is essential for initial module setup, including import statements and custom mount names. It serves as a blueprint, outlining how the module integrates with the broader application architecture.

    Example:

    module;
  • entities.rell: Contains the structural backbone, housing entities, enums, and structures (for modules with up to three structs). It also includes functions for retrieving entities by ID, offering a quick reference to the module's fundamental data structures.

    Example:

    entity house {
    index street;
    number: integer;
    key street, number;
    number_of_rooms: integer;
    number_of_floors: integer;
    floor_area: integer;
    index number_of_rooms, floor_area;
    }
  • operations.rell: Lists all the operations the module can perform. These operations are the actionable components, enabling the module to execute its primary functions.

    Example:

    operation create_house(
    street_id: rowid,
    number: integer,
    number_of_rooms: integer,
    number_of_floors: integer,
    floor_area: integer
    ) {
    val street = street @ { .rowid == street_id };
    create house ( street, number, number_of_rooms, number_of_floors, floor_area );
    }
  • queries.rell: A repository of all queries within the module, dictating how it communicates with the database to fetch and present data.

    Example:

    query get_houses_with_streets() = ( house, street) @* { street == house.street } ( street = .street.address, .number );
  • functions.rell: Encapsulates the core of the module's business logic. This file contains a variety of functions that define how the module processes data and responds to different inputs.

    Example:

    function require_user(id: byte_array) = require(user @? { id }, "User with id %b does not exist".format(id));
  • structs.rell: For modules containing more than three structs, this file provides a dedicated space to maintain clarity and organization, especially in complex modules.

    Example:

    struct user {
    name: text;
    address: text;
    mutable balance: integer = 0;
    }

Import

To access the module's definitions, you need to import the module:

import app.single;

function test() {
single.f(); // Calling the function "f" defined in the module "app.single".
}

When importing a module, it's added to the current namespace with some alias. By default, the alias is the last part of the module name, single for the module app.single or multi for app.multi. The definitions of the module can be accessed via the alias.

A custom alias can be specified:

import alias: app.multi;

function test() {
alias.g();
}

It's possible to specify a relative name of a module when importing. In that case, the name of the imported module is derived from the current module's name. For example, if the current module is a.b.c,

  • import .d; imports a.b.c.d
  • import alias: ^; imports a.b
  • import alias: ^^; imports a
  • import ^.e; imports a.b.e
tip

When importing a function from a module, import the specific module containing the desired function and explicitly call it using the module name as a namespace. This ensures clarity and eliminates ambiguity. For example, if the map_entity function exists in both users and projects modules. Importing both modules and directly calling map_entity leads to ambiguity, as it's unclear which function is being invoked.

Wildcard imports

Importing all definitions of a module:

import foo.*;

All definitions are added directly to the importing namespace.

It's possible to import definitions of a specific namespace defined within a module:

import foo.{ns.*};

An import alias, if specified, creates a nested namespace and adds imported definitions there:

import sub: foo.{ns.*};

Definitions from the namespace ns of module foo in this example are added to a new namespace sub.

Import specific definitions

To import a specific definition (or a set of definitions) from a module, specify their names in braces:

import foo.{f};
import foo.{g, h};

The definitions "f", "g", and "h" are added to the importing namespace like they were defined there.

If an import alias is specified, a nested namespace is created:

import ns: foo.{f, g};

This creates a namespace ns containing definitions f and g.

One can specify an alias for individual definitions in braces:

import foo.{a: f, b: g};

Imported definitions in this example are added to the namespace under names a and b.

Run-time

At run-time, not all modules defined in a source directory tree are active. Only the main module and all modules it imports (directly or indirectly) are active. There is a main module that is specified when starting a Rell app.

When a module is active, its operations and queries can be invoked, and tables for its entities and objects are added to the database on initialization.