Gerust

repository·main·Indexed 41 days ago

https://github.com/mainmatter/gerust

A project generator for Rust backend applications that provides a structured architecture using axum, SQLx, and PostgreSQL. It simplifies crate separation, database migrations, and error handling, and includes a CLI for scaffolding entities, controllers, and middleware. Version 0.0.7.

Tokens
31.4K
Snippets
116
Records
143
Agent score
50%

What's inside Gerust

  1. Understand the `config` crate and the `Config` struct

    main

    The config crate manages application configuration at runtime. It uses the Config struct to hold all configuration values, which are parsed from a hierarchy of TOML files and environment variables using the figment crate.

    By default, the Config struct includes server and database configurations. You can extend it by adding your own fields to the struct. If your project does not use a database, the database field can be omitted.

    #[derive(Deserialize, Clone, Debug)]
    pub struct Config {
        pub server: ServerConfig,
        pub database: DatabaseConfig, // The database configuration only exists for projects that use a database
        // add your config settings here…
    }
  2. Choose a project type: Minimal vs Full

    main

    When starting a new project with Gerust, you must choose between two architectural patterns based on whether your system requires a database:

    Minimal Projects

    Use this pattern if your project does not require a database. A minimal project includes:

    • A web server
    • Configuration reading functionality
    • CLI tooling for creating controllers and project files

    Full Projects

    Use this pattern for standard applications that require a database. In addition to the web server, configuration, and CLI tools, full projects include:

    • Entity definitions
    • Changesets
    • Database access layers
    • Validations
  3. Manage application environments

    main

    The config crate determines the execution environment, which affects how configuration is loaded. Gerust supports three environments:

    1. development: The default environment.
    2. test: Automatically used when running tests.
    3. production: Used for live deployments.

    How to set the environment

    You can manually specify the environment using one of two methods:

    • Environment Variable: Set APP_ENVIRONMENT to your desired environment name.
    • CLI Argument: Pass an argument to commands provided by the cli crate.
  4. Understand the `db` crate architecture

    main
    The db crate is automatically generated for projects using a database. Instead of using a traditional ORM, Gerust uses sqlx with PostgreSQL to interact with the database using plain structs and functions. This approach avoids the accidental complexity of ORMs by defining entities as simple structs and providing explicit functions for data access. Queries are checked for correctness at compile time using sqlx's compile-time checked queries.
  5. Validate data using Changesets

    main

    Instead of validating entities directly, Gerust uses a changeset architecture. Data is validated within a changeset struct before it is applied to an existing entity (for updates) or converted into a new entity (for inserts).

    Validations are implemented using the validator crate and are declared using the #[validate(...)] attribute on the changeset fields.

    Example: Creating a user with validation

    #[derive(Deserialize, Validate, Clone)]
    pub struct UserChangeset {
        #[validate(length(min = 1))]
        pub name: String,
    }
    
    pub async fn create(
        user: UserChangeset,
        executor: impl sqlx::Executor<'_, Database = Postgres>,
    ) -> Result<User, crate::Error> {
        user.validate()?;
    
        let record = sqlx::query!(
            "INSERT INTO users (name) VALUES ($1) RETURNING id",
            user.name
        )
        .fetch_one(executor)
        .await
        .map_err(|e| crate::Error::DbError(e.into()))?;
    
        Ok(User {
            id: record.id,
            description: user.name,
        })
    }
  6. Testing API endpoints with #[db_test]

    main

    Gerust provides a #[db_test] macro (from the macros crate) for testing API endpoints. Unlike standard Rust tests, #[db_test] creates a fresh, isolated database for every individual test case using a template from your main test database. This ensures tests do not interfere with each other.

    Key features:

    • Isolation: Each test gets its own dedicated database that is automatically deleted after the test completes.
    • Context: Test functions receive a DbTestContext argument, which provides access to both the dedicated db_pool and the application instance (app) configured to use that specific database.
    • Helpers: The context.app object provides helper methods like .request() to simulate HTTP requests.
    #[db_test]
    async fn test_read_all(context: &DbTestContext) {
        // 1. Setup state using the test database pool
        let changeset: entities::notes::NoteChangeset = Faker.fake();
        entities::notes::create(changeset.clone(), &context.db_pool)
            .await
            .unwrap();
    
        // 2. Execute request via the app helper
        let response = context
            .app
            .request("/notes")
            .send()
            .await;
    
        // 3. Assert results
        assert_that!(response.status(), eq(StatusCode::OK));
    }
  7. Implement database entities and changesets in the `db` crate

    main

    The db crate uses SQLx with PostgreSQL (no ORM). Data is managed through two primary structures:

    1. Entities: Simple structs representing a database record (e.g., Task).
    2. Changesets: Structs used for validating and preparing data for insertion or updates. Changesets use the validator crate for logic (e.g., #[validate(length(min = 1))]).

    To persist data, you implement functions that take a changeset and an executor (implementing sqlx::Executor). The function should validate the changeset before executing the SQL query.

    Example pattern:

    • load(id, executor): Retrieves an entity by ID.
    • create(changeset, executor): Validates the changeset and inserts a new record.
    #[derive(Serialize, Debug, Deserialize)]
    pub struct Task {
        pub id: Uuid,
        pub description: String,
    }
    
    #[derive(Deserialize, Validate, Clone)]
    pub struct TaskChangeset {
        #[validate(length(min = 1))]
        pub description: String,
    }
    
    pub async fn create(
        task: TaskChangeset,
        executor: impl sqlx::Executor<'_, Database = Postgres>,
    ) -> Result<Task, crate::Error> {
        task.validate()?;
    
        let record = sqlx::query!(
            "INSERT INTO tasks (description) VALUES ($1) RETURNING id",
            task.description
        )
        .fetch_one(executor)
        .await
        .map_err(|e| crate::Error::DbError(e.into()))?;
    
        Ok(Task {
            id: record.id,
            description: task.description,
        })
    }
  8. How to perform isolated database testing with `#[db_test]`

    main

    Gerust provides a #[db_test] macro to enable parallel test execution with database isolation. Every test marked with this macro runs in its own unique database, which is created as a copy of the main test database and destroyed automatically after the test completes.

    To use it, define an async test function that accepts a &DbTestContext. This context provides:

    • app: The axum::Router for the application.
    • db_pool: A connection pool connected to the test-specific database.

    This allows you to seed the database using the db_pool and then make HTTP requests against the app to verify the state.

    #[db_test]
    async fn test_read_all(context: &DbTestContext) {
        let task_changeset: TaskChangeset = Faker.fake();
        create_task(task_changeset.clone(), &context.db_pool)
            .await
            .unwrap();
    
        let response = context
            .app
            .request("/tasks")
            .method(Method::GET)
            .send()
            .await;
    
        assert_that!(response.status(), eq(StatusCode::OK));
    
        let tasks: TasksList = response.into_body().into_json::<TasksList>().await;
        assert_that!(tasks, len(eq(1)));
        assert_that!(
            tasks.first().unwrap().description,
            eq(task_changeset.description)
        );
    }
  9. Use Changesets for data manipulation

    main

    Data manipulation in Gerust is performed using Changesets. A changeset is a companion struct to an entity that contains only the fields that are editable (for example, excluding an auto-assigned id field).

    Changesets are used with the validator crate to implement validations. Before performing a database operation like create or update, you must call .validate()? on the changeset to ensure the data meets the required constraints.

    // Example of a changeset with validation
    #[derive(Deserialize, Validate, Clone)]
    pub struct TaskChangeset {
        #[validate(length(min = 1))]
        pub description: String,
    }
    
    // Usage in a create function
    pub async fn create(
        task: TaskChangeset,
        executor: impl sqlx::Executor<'_, Database = Postgres>,
    ) -> Result<Task, crate::Error> {
        task.validate()?; // Validate the changeset before proceeding
        // ... database logic
    }
  10. Understand the Gerust project structure

    main

    Gerust uses Cargo workspaces to organize the system into several specialized crates. Depending on your project needs, you will interact with these components:

    • cli: CLI tools for database migrations and project file generation.
    • config: Manages the Config struct and handles configuration from TOML files and environment variables.
    • db: (Optional) Handles database access, migrations, and entity definitions using SQLx.
    • macros: Provides utility macros like #[db_test] for application testing.
    • web: The main axum application, containing controllers, middlewares, routes, and the server entrypoint.
  11. Use Gerust macros for application testing

    main

    When writing application tests in a Gerust project, use the test and db_test macros provided by the macros crate instead of the standard tokio::test macro.

    These macros wrap tokio's testing functionality but add Gerust-specific lifecycle management:

    1. test macro: Automatically creates a new instance of your application and injects it into your test function via the test context.
    2. db_test macro: Extends the test macro by creating a dedicated, isolated database for the test. This database is cloned from your main test database template. The macro ensures the database is automatically deleted once the test completes to prevent clutter.
    // Note: Actual usage syntax depends on the web crate implementation,
    // but the concept involves replacing #[tokio::test] with Gerust's macros.
    
    #[gerust::db_test]
    async fn my_application_test(ctx: TestContext) {
        // ctx contains the application instance and the isolated database
    }
  12. Understand the minimal project structure

    main

    A minimal project is organized into several crates:

    • cli: Contains a binary (generate.rs) for creating project files like controllers or middlewares.
    • config: Handles reading and managing configuration values via a Config struct. It includes app.toml and environment-specific files in environments/ (e.g., development.toml, production.toml, test.toml).
    • macros: Provides macros specifically used for testing.
    • web: The HTTP interface implementation, which is a standard axum application. It contains controllers/, middlewares/, routes.rs, and state.rs.
    • tests/: Contains API tests.