tl;dr:

Heave is a lightweight Rust library for easily saving and loading your structs to a SQLite database. It's designed for small personal projects where a full ORM is overkill, acting as a simple bridge to persist your data.

Introducing Heave: A Lightweight Bridge for Your Rust Structs

Have you ever been working on a small personal project in Rust and wished for a simple way to store your data without the overhead of a full-blown Object-Relational Mapper (ORM)? Sometimes you just need to save the state of your structs and get them back later, without wrestling with complex database schemas. If that sounds familiar, you might find a friend in Heave (github repo).

What is Heave?

Heave is a small Rust library designed with a specific purpose in mind: to provide a simple and flexible way to persist your data structures. At its core, Heave implements an Entity-Attribute-Value (EAV) model. Think of it as a way to break down your complex objects into simple, storable pieces.

The Magic Inside: From Structs to Fluid Entities

The central idea behind Heave is the transformation of your standard Rust structs into what we call "fluid entities" and back again. This is where the EAV trait comes into play. By implementing this trait for your struct, you create a simple mapping that tells Heave how to translate your data into a more generic, database-friendly format.

This means you can design your application with clean, strongly-typed Rust structs, and then use Heave to seamlessly persist them into a SQLite database. When you need them again, Heave can reverse the process, rehydrating your structs from the stored data.

Mapping a Product struct to and from Entity

use heave::*;

// Define a struct named `Product` to represent a product.
struct Product {
    // `id` is a public field of type `String` to uniquely identify the product.
    pub id: String,
    // `name` is a public field of type `String` for the product's name.
    pub name: String,
    // `model` is a public optional field of type `String` for the product's model.
    pub model: Option<String>,
    // `price` is a public field of type `i64` for the product's price.
    pub price: i64,
}

// Implement the `EAV` trait for the `Product` struct.
impl EAV for Product {
    // `class` is a function that returns the class name of the entity.
    fn class() -> &'static str {
        "product"
    }
}

// Implement the `From<Product>` trait for the `Entity` struct.
impl From<Product> for Entity {
    // `from` is a function that converts a `Product` into an `Entity`.
    fn from(value: Product) -> Entity {
        // Create a new `Entity` for the `Product` class.
        Entity::new::<Product>()
            // Set the entity's ID from the product's ID.
            .with_id(&value.id)
            // Add the "name" attribute with the product's name.
            .with_attribute("name", value.name)
            // Add the optional "model" attribute with the product's model.
            .with_opt_attribute("model", value.model)
            // Add the "price" attribute with the product's price.
            .with_attribute("price", value.price)
    }
}

// Implement the `From<Entity>` trait for the `Product` struct.
impl From<Entity> for Product {
    // `from` is a function that converts an `Entity` into a `Product`.
    fn from(value: Entity) -> Self {
        // Create a new `Product` from the entity's attributes.
        Self {
            // Set the product's ID from the entity's ID.
            id: value.id.clone(),
            // Unwrap the "name" attribute to get the product's name.
            name: value.unwrap("name"),
            // Unwrap the optional "model" attribute to get the product's model.
            model: value.unwrap_opt("model"),
            // Unwrap the "price" attribute to get the product's price.
            price: value.unwrap("price"),
        }
    }
}

fn main() {
    // Define the path for the SQLite database file.
    let db_path = "./simple_product.sqlite3";
    // Create a new `Catalog` instance with the specified database path.
    let mut catalog = Catalog::new(db_path);
    // Initialize the catalog, which sets up the database.
    catalog.init().unwrap();
    // Create a new `Product` instance representing a laptop.
    let new_laptop = Product {
        id: "LT001".to_string(),
        name: "SuperPenguin".to_string(),
        model: Some("Mark III.2".to_string()),
        price: 125000,
    };
    // Insert the new laptop into the catalog. Note that at this time the product is in memory.
    catalog.insert(new_laptop);
    // Persist the changes in the catalog to the database.
    catalog.persist().unwrap();
    // Remove the SQLite database file.
    std::fs::remove_file(db_path).unwrap();
}

Built for Your Personal Projects

Heave is intentionally lightweight. It uses SQLite as its backend, which is perfect for small-scale applications, command-line tools, or personal projects where you need a simple, file-based database without the hassle of setting up a larger database server.

What Heave is Not

It's important to understand that Heave is not a full-fledged ORM. It doesn't come with complex query builders, relationship management, or automatic schema migrations. It's a focused tool that does one thing well: persisting your Rust structs in a simple and flexible manner.

If you're looking for a straightforward solution for data persistence in your next Rust project, give Heave a try. It might just be the lightweight bridge you need between your application and your database.


See also...


tags: #rust, #database, #project:heave