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;
importsa.b.c.d
import alias: ^;
importsa.b
import alias: ^^;
importsa
import ^.e;
importsa.b.e
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.