Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

repox

Introduction

repox:: is a trait interface framework for building repositories with different kinds of data access needs. This crate has three main goals:

  • Provide application developers simple traits that describe what kind data access interface they need between entities and a repository.
  • Supply tooling to make implementing and defining repositories and entities much easier by removing a lot of needless boilerplate.
  • Maintain thorough, high-quality, documentation to lower the cognitive load on needing to remember constantly how to use features of this crate.

Simple Blog Example

Let’s say you want to model blog posts for authors. Here is a simple example of how you might use repox:: to define your entities and repository interface for your application. This example demonstrates how to use the various traits and how they are used.

// Define some simple entities for a blog application

#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[has_many(Post.author_id)]
#[create_params(AuthorParams)]
pub struct Author {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[belongs_to(Author, author_id)]
#[create_params(PostParams)]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub title: String,
    pub content: String,
}

// Define the repository interface for the blog entities

#[repox::mockall] // <- Testing as a first-class citizen with mockall
pub trait BlogRepo:
    repox::Repo

    // Only create is needed for authors in this example
    + repox::CreateWith<Author, AuthorParams>

    // Posts can be created, read with an author, updated, and deleted
    + repox::CreateWith<Post, PostParams>
    + repox::FetchWithParentById<Post, Author>
    + repox::UpdateById<Post>
    + repox::DeleteById<Post>
{
}

// Example usage of the BlogRepo

async fn example_usage(repo: &impl BlogRepo) -> anyhow::Result<()> {
    // creating an author
    let author_params = AuthorParams { name: "GhostWriter".into() };
    let ghosty = repo.create_with(author_params).await?;

    // giving them a post
    let post_params = PostParams {
        author_id: ghosty.id,
        title: "Scary Post".into(),
        content: "Booo!".into(),
    };
    let boo = repo.create_with(post_params).await?;

    // updating the post
    let mut more_boo = boo.clone();
    more_boo.content = "Booooooooooooo!".into();
    repo.update_by_id(more_boo.clone()).await?;

    // fetching post with its author
    let (post, author) = repo.fetch_with_parent_by_id(boo.id).await?;
    assert_eq!(post, more_boo);
    assert_eq!(author, ghosty);

    // removing the post
    repo.delete_by_id(boo.id).await?;

    // realizing this code compiles 🀯
    Ok(())
}

// Now bear witness to the power of documentation. Behold how we
// can mock out the BlogRepo and test our example usage function
// with nary a line of implementation code written!
let mut blog = MockBlogRepo::new();

// I personally like to import these helpers, but you do you
use repox::mock::{ok_with, ok_val};

// an author is born
blog.expect_create_with()
    .returning(ok_with(|params: AuthorParams| {
        Author { id: 1, name: params.name }
    }));

// their first post... will be created
blog.expect_create_with()
    .returning(ok_with(|params: PostParams| {
        Post {
            id: 1,
            author_id: params.author_id,
            title: params.title,
            content: params.content,
        }
    }));

// an update will be made to the post, as it was foretold in the example
blog.expect_update_by_id::<Post>()
    .returning(ok_val(()));

// data will be extracted for verification of the post and author
blog.expect_fetch_with_parent_by_id()
    .returning(ok_with(|id| (
        Post {
            id,
            author_id: 1,
            title: "Scary Post".into(),
            content: "Booooooooooooo!".into(),
        },
        Author {
            id: 1,
            name: "GhostWriter".into(),
        }
    )));

// in a blind rage of drunken power; a deletion will occur...
blog.expect_delete_by_id::<Post>()
    .returning(ok_val(::repox::DeleteStatus::Deleted));

// Now all will happen as it was documented, and our example
// usage will be verified by these tests... That's Hawt πŸ”₯
pollster::block_on(async {
example_usage(&blog).await.expect("Demo to work!");
});

Documentation Prayer

By the documentation, they shall be known.
With the documentation, they will be used.

May the documented features of this project
be a beacon to both man **and** machine. Let
us rejoice as we read detailed documentation,
and let us rejoice even more as we write it.

By the Holy Documentation of the Omnissiah,
may our code be free of heresy and our features
catalogued in the Codex Repox. For the Machine
Spirit rejoices in well-commented functions,
and the Tech-Priests sing praises to the README
eternal!

By the documentation, they shall be known.
With the documentation, they will be used.

repox::Repo Interface Methods

Introduction

repox::Repo is the main trait interface for the repox framework. Basic CRUD1 operations are supported by repository traits, such as DeleteById<T>, Insert<T>, and many others. These allow application developers to define simple repository interfaces which can be implemented by any back-end, like SQL databases, NoSQL, or even in-memory data structures.


  1. Create, Read, Update, Delete ↩

create_with

πŸ“ Method Info

create_with(params: Params) -> Result<Entity, anyhow::Error>

Provided to any repository that implements CreateWith, this method allows you to create a new entity using a separate parameters struct. This is particularly useful when the creation of an entity requires additional information that is not part of the entity itself, such as auto-generated IDs or timestamps.

🧩 Detailed Example:

I just got off the phone with the manager of our local widget factory and it turns out they need to start storing their widget data immediately! Let’s see how we can use create_with to get them up and running.

use repox::Repo;
use std::sync::atomic::Ordering;

// Define an entity, take note of the `create_params` attribute
// as it creates a new struct called `WidgetParams` that we can
// use with the `create_with` interface
#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[create_params(WidgetParams)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

// an interface to start building these puppies
pub trait WidgetCreator: Repo + repox::CreateWith<Widget, WidgetParams> {}

// simple structure to implement WidgetCreator and start making
// bank on this sweet deal.
#[derive(Debug, Default)]
pub struct WidgetRepo {
    pub data: dashmap::DashMap<u32, Widget>,
    pub next_num: std::sync::atomic::AtomicU32,
}

// lets make bank on this sweet deal by implementing our trait on
// on this sick new repo trait Claude code made for us:
impl Repo for WidgetRepo {}
impl WidgetCreator for WidgetRepo {}

// this is where we pull in that sweet money with this ID tracking
impl repox::CreateWith<Widget, WidgetParams> for WidgetRepo {
    async fn exec(&self, params: WidgetParams) -> anyhow::Result<Widget> {
        let widget = Widget {
            id: self.next_num.fetch_add(1, Ordering::SeqCst),
            name: params.name,
        };
        self.data.insert(widget.id, widget.clone());
        Ok(widget)
    }
}

// we better test this... just to make sure we're guchi
let repo = WidgetRepo::default();
let params = WidgetParams { name: "RamRod".into() };

// pump one and make sure it made it in
pollster::block_on(async {
let ram_rod = repo.create_with(params.clone()).await.unwrap();
assert_eq!(ram_rod.name, "RamRod");
assert_eq!(ram_rod.id, 0);

// better pump in another and make sure the ids are working right
let another_rod = repo.create_with(params.clone()).await.unwrap();
assert_eq!(another_rod.name, "RamRod");
assert_eq!(another_rod.id, 1);

// nice!, but did the data make it in?
let first_rod = repo.data.get(&0).unwrap();
let second_rod = repo.data.get(&1).unwrap();
assert_eq!(*first_rod, ram_rod);
assert_eq!(*second_rod, another_rod);

// Sick! 🀘
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(WidgetParams)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[repox::mockall]
pub trait WidgetCreator: Repo + repox::CreateWith<Widget, WidgetParams> {}

let params = WidgetParams { name: "RamRod".into() };

let mut repo = MockWidgetCreator::new();
repo.expect_create_with::<Widget, WidgetParams>()
    .withf(|params| params.name == "RamRod")
    .returning(repox::mock::ok_with(|params: WidgetParams| {
        Widget { id: 42, name: params.name }
    }));

pollster::block_on(async {
let widget = repo.create_with(params).await.unwrap();
assert_eq!(widget.name, "RamRod");
assert_eq!(widget.id, 42);
});

// It really brings the whole piece together 🫰

delete_by_id

πŸ“ Method

delete_by_id(id: ID) -> Result<DeleteStatus, anyhow::Error>

Allows you to delete an entity from the repository using its unique identifier. This method is useful when you want to remove a specific entity without needing to retrieve it first. This requires that your repository implements [DeleteById<T>] for the entity type T you want to delete. The [DeleteStatus] variant returned by this method indicates whether the deletion actually removed an entity or if no entity was found with the provided ID.

🧩 Detailed Example:

use repox::DeleteStatus;
use repox::Repo;

// define an entity
#[derive(Debug, Clone, PartialEq, repox::Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

// simple repository that can only delete widgets by ID
pub trait WidgetDeletor: repox::Repo + repox::DeleteById<Widget> {}

// simple new-type wrapper around DashMap to implement WidgetDeletor
#[derive(Debug, Clone, Default)]
pub struct WidgetRepo {
    pub data: dashmap::DashMap<u32, Widget>,
}

// tag WidgetRepo as a `repox::Repo`
impl repox::Repo for WidgetRepo {}

// now it can be tagged as a WidgetDeletor
impl WidgetDeletor for WidgetRepo {}

// produces a compile time error stating that the following
// trait is not satisfied; so we here it is:
impl repox::DeleteById<Widget> for WidgetRepo {
    async fn exec(&self, id: u32) -> Result<DeleteStatus, anyhow::Error>
    {
        let widget = self.data.remove(&id);
        return Ok(if widget.is_some() {
            DeleteStatus::Deleted
        } else {
            DeleteStatus::NotFound
        })
    }
}

// now we revel in the glory of being able to delete widgets by ID
let widget = Widget {
    id: 42,
    name: "SteamBlaster".into(),
};

// start up our new awesome repo and load in our widget
let repo = WidgetRepo::default();
repo.data.insert(widget.id, widget.clone());

// now let the world see the power of delete_by_id in action
pollster::block_on(async {
let first_delete = repo.delete_by_id(widget.id).await.unwrap();
assert_eq!(first_delete, DeleteStatus::Deleted);

let second_delete = repo.delete_by_id(widget.id).await.unwrap();
assert_eq!(second_delete, DeleteStatus::NotFound);
});

// let's snoop into the data and make sure it was removed
assert!(repo.data.get(&widget.id).is_none());

// Wizard! πŸ§™β€β™‚οΈ

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(WidgetParams)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[repox::mockall]
pub trait DeleteRepo: Repo + repox::DeleteById<Widget> {}

let mut repo = MockDeleteRepo::new();
repo.expect_delete_by_id::<Widget>()
    .withf(|id| *id == 42)
    .returning(repox::mock::ok_val((repox::DeleteStatus::Deleted)));

pollster::block_on(async {
let status = repo.delete_by_id::<Widget>(42).await.unwrap();
assert_eq!(status, repox::DeleteStatus::Deleted);
});

// We made it! 🏁

fetch_by_id

πŸ“ Method

fetch_by_id(id: ID) -> Result<Entity, repox::FetchError<Entity>>

Fetches a single entity from the repository using its unique identifier. This will return Ok(entity) if an entity with the given ID exists. The Err variant will be a FetchError which can be used to determine if the fetch failed because the entity was not found.

🧩 Detailed Example:

The suites at the top are getting a little worried at not being able to to recall widgets that have been created. Let’s give them access to the fetch_by_id method by implementing the [FetchById<T>] trait for our WidgetData repository.

This will allow us to retrieve widgets using their unique identifier, which is essential for many applications where you need to access specific entities without fetching the entire collection.

use repox::Repo;

// Define an entity
#[derive(Debug, Clone, PartialEq, repox::Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

// We can fetch them by ID and enjoy them for all time
pub trait FetchingRepo: Repo + repox::FetchById<Widget> {}

// Our new high-end data store that only works with widgets
#[derive(Debug, Default)]
pub struct WidgetData {
    pub data: dashmap::DashMap<u32, Widget>,
}

// lets put all the worry to rest by implementing the necessary
// traits to make this a fetching repo for any occasion
impl Repo for WidgetData {}
impl FetchingRepo for WidgetData {}

impl repox::FetchById<Widget> for WidgetData {
    async fn exec(&self, id: u32)
    -> Result<Widget, repox::FetchError<Widget>>
    {
        match self.data.get(&id) {
            Some(widget_ref) => Ok(widget_ref.clone()),
            None => Err(repox::FetchError::NotFound(id)),
        }
    }
}

// okay, cool, but we should make sure it works ya know?
pollster::block_on(async {
let repo = WidgetData::default();
let widget = Widget {
    id: 42,
    name: "FistFullOfDollars".into(),
};
repo.data.insert(widget.id, widget.clone());

// Hold on to your butts, because we're about to
// fetch this fist full of dollars by ID!
let ffod = repo.fetch_by_id(42).await.unwrap();
assert_eq!(ffod, widget);

// Tight! πŸ•Ί
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[repox::mockall]
pub trait WidgetFetcher: Repo + repox::FetchById<Widget> {}

let mut repo = MockWidgetFetcher::new();
repo.expect_fetch_by_id::<Widget>()
    .withf(|id| *id == 42)
    .returning(repox::mock::ok_with(|id: u32| Widget {
        id,
        name: "TableRocker".into(),
    }));

pollster::block_on(async {
let widget = repo.fetch_by_id::<Widget>(42).await.unwrap();
assert_eq!(widget.id, 42);
assert_eq!(widget.name, "TableRocker");
});

// I'd buy that for a dollar! πŸ’΅

fetch_by_id_optional

πŸ“ Method

fetch_by_id_optional(id: ID) -> Result<Option<Entity>, anyhow::Error>

Available when your repository implements the FetchById<T> trait for the entity type T you want to fetch. This method allows you to attempt to fetch an entity by its unique identifier, returning Ok(Some(entity)) if found, Ok(None) if not found, and Err(error) if there was an error during the fetch operation.

This is particularly useful when you want to handle the case of a missing entity gracefully without treating it as an error condition.

🧩 Detailed Example:

A harrowing message just came in. It turns out one of our vendors has no idea which widgets were discontinued and which ones are still in production. For now they need to be able to optionally fetch widgets by ID. Thankfully all we need to do is implement the simple FetchById<T> trait for our widget repo and we can get this for free.

use repox::Repo;

// Define an entity
#[derive(Debug, Clone, PartialEq, repox::Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

// We can fetch them by ID and enjoy them for all time
pub trait FetchingRepo: Repo + repox::FetchById<Widget> {}

// Our new high-end data store that only works with widgets
#[derive(Debug, Default)]
pub struct WidgetData {
    pub data: dashmap::DashMap<u32, Widget>,
}

// lets put all the worry to rest by implementing the necessary
// traits to make this a fetching repo for any occasion
impl Repo for WidgetData {}
impl FetchingRepo for WidgetData {}

impl repox::FetchById<Widget> for WidgetData {
    async fn exec(&self, id: u32)
    -> Result<Widget, repox::FetchError<Widget>>
    {
        match self.data.get(&id) {
            Some(widget_ref) => Ok(widget_ref.clone()),
            None => Err(repox::FetchError::NotFound(id)),
        }
    }
}

// okay, cool, but we should make sure it works ya know?
pollster::block_on(async {
let repo = WidgetData::default();
let widget = Widget {
    id: 42,
    name: "FistFullOfDollars".into(),
};
repo.data.insert(widget.id, widget.clone());

// Hold on to your butts, because we're about to
// fetch this fist full of dollars by ID, but optionally!
let ffod = repo.fetch_by_id_optional(42).await.unwrap();
assert_eq!(ffod, Some(widget));

// Now we need to make sure it returns None when the widget isn't found
let data = repo.fetch_by_id_optional(100).await.unwrap();
assert_eq!(data, None);

// Legendary! πŸͺ©
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(WidgetParams)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[repox::mockall]
pub trait MaybeWidget: Repo + repox::FetchById<Widget> {}

let mut repo = MockMaybeWidget::new();
repo.expect_fetch_by_id_optional::<Widget>()
    .withf(|id| *id == 42)
    .returning(repox::mock::ok_val(None));

pollster::block_on(async {
let maybe_widget = repo.fetch_by_id_optional::<Widget>(42).await.unwrap();
assert!(maybe_widget.is_none());
});

// Smooth like 🧈

fetch_with_children_by_id

πŸ“ Method

fetch_with_children_by_id(id: ID) -> Result<
    (Entity, Vec<ChildEntity>),
    repox::FetchWithChildrenError<Entity>,
>

This method allows you to fetch an entity along with its associated child entities using the unique identifier of the parent entity. This is available when your repository implements the FetchWithChildrenById<ParentEntity, ChildEntity> trait. The method returns a tuple containing the parent entity and a vector of its child entities if found, or an error if the parent entity is not found.

🧩 Detailed Example:

Fudge monkeys! It turns out that widgets were supposed to be able to contain provisions. Fred ran off muttering something about becoming a cabbage farmer and leaving the city life behind. All you can find out is that these provisions are specific float values… but that’s all we know right now. This is it, it’s all up to us now.

use repox::Repo;

// Define an entity
#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[has_many(WidgetProvision.widget_id)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

// Define an entity for now
#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[belongs_to(Widget, widget_id)]
pub struct WidgetProvision {
    pub id: u128,
    pub widget_id: u32,
    pub float: f64,
}

// We've made provisions for our widgets, get them out there!
pub trait WidgetWithProvisions: Repo
    + repox::FetchWithChildrenById<Widget, WidgetProvision>
{
}

// Our new high-end data store that only works with widgets
// and could could be done better if we had more time _<murmurs>_
#[derive(Debug, Default)]
pub struct WidgetData {
    // what could possibly go wrong with this? I mean... it's just a
    // map of widgets to their provisions, what could go wrong?
    pub widgets: dashmap::DashMap<u32, (Widget, Vec<WidgetProvision>)>,
}

// lets make bank on this sweet deal by implementing our trait on
// on this sick new repo trait Claude code made for us:
impl Repo for WidgetData {}
impl WidgetWithProvisions for WidgetData {}

// this is it; the time where claude make's it's magic and implements
// the interface for us and we can finally fetch widgets with their
// provisions by ID... take that Sqlite!!
impl repox::FetchWithChildrenById<Widget, WidgetProvision> for WidgetData {
    async fn exec(&self, id: u32) -> Result<
        (Widget, Vec<WidgetProvision>),
        repox::FetchWithChildrenError<Widget>,
    > {
        match self.widgets.get(&id) {
             Some(data_ref) => Ok(data_ref.clone()),
             None => Err(repox::FetchWithChildrenError::NotFound(id)),
        }
    }
}

// now I want them to marvel at her speed, go out with a bang!
pollster::block_on(async {
let data = WidgetData::default();
data.widgets.insert(1, (
    Widget { id: 1, name: "Horn".into() },
    vec![WidgetProvision { id: 7, widget_id: 1, float: 3.14 }]
));

// get on the horn and fetch this widget with its provisions by ID!
let (horn, povisions) = data.fetch_with_children_by_id(1).await.unwrap();
assert_eq!(horn, Widget { id: 1, name: "Horn".into() });
assert_eq!(
    povisions,
    vec![WidgetProvision { id: 7, widget_id: 1, float: 3.14 }]
);

// We're the renegades of data fetching, the outlaws of the repo world,
// and fetch_with_children_by_id is our trusty steed! 🐎
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
#[has_many(Doodad.widget_id)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Doodad {
    pub id: u32,
    pub widget_id: u32,
}

#[repox::mockall]
pub trait ChildRepo: Repo + repox::FetchWithChildrenById<Widget, Doodad> {}

let mut repo = MockChildRepo::new();
repo.expect_fetch_with_children_by_id::<Widget, Doodad>()
    .withf(|id| *id == 42)
    .returning(repox::mock::ok_with(|id: u32| (
        Widget {
            id,
            name: "Sprocket".into(),
        },
        vec![Doodad { id: 1, widget_id: id }],
    )));

pollster::block_on(async {
let (widget, doodads) = repo
    .fetch_with_children_by_id::<Widget, Doodad>(42).await.unwrap();
assert_eq!(widget, Widget { id: 42, name: "Sprocket".into() });
assert_eq!(doodads, vec![Doodad { id: 1, widget_id: 42 }]);
});

// Take that to the bank! 🏦

fetch_with_parent_by_id

πŸ“ Method

fetch_with_parent_by_id(id: ID) -> Result<
    (Entity, ParentEntity),
    repox::FetchWithParentError<Entity>,
>

If you implement the FetchWithParentById<ChildEntity, ParentEntity> trait, you can use this method to fetch a child entity along with its associated parent entity using the unique identifier of the child. The method returns a tuple containing the child entity and its parent entity if found, or an error if the child entity is not found. This is particularly useful when you need to access related data in a single fetch operation, reducing the number of queries needed to retrieve associated entities.

🧩 Detailed Example:

Cheese and Crackers! It turns out that widgets were supposed to each belong to certain assemblies. Nobody at WidgetCo. can remember exactly what these assemblies are, but they know that they we definitely need them. The head of their IT refuses to talk to you, and has simply instructed you to β€œfigure it out”. We don’t understand this binary format so for now we’ll just house it in a Vec<u8> and hope for the best.

use repox::Repo;

// Our tried and true entity definition, but now with a parent relationship!
// The `belongs_to` macro tag is how we acknowledge this relationship in
// our code.
#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[belongs_to(Assembly, assembly_id)]
pub struct Widget {
    pub id: u32,
    pub assembly_id: u32,
    pub name: String,
}

// The fabled assembly entity.  We know nothing of its kind;
// however, we do know that it has a relationship with widgets.
// Here we acknowledge this relationship with the `has_many`
// macro tag pointing to the `assembly_id` field of the
// `Widget` struct.
#[derive(Debug, Clone, PartialEq, repox::Entity)]
#[has_many(Widget.assembly_id)]
pub struct Assembly {
    pub id: u32,
    pub data: Vec<u8>,
}

// the repo needs to know about this relationship too, so we
// make a trait that can support them together
pub trait WidgetWithAssembly: Repo
    + repox::FetchWithParentById<Widget, Assembly>
{
}

// Our new high-end data store that only works with widgets
// freshly outfitted with the ability to hold assemblies for
// these widgets.
#[derive(Debug, Default)]
pub struct WidgetData {
    pub widgets: dashmap::DashMap<u32, Widget>,
    pub assemblies: dashmap::DashMap<u32, Assembly>,
}

// enough gabbing, let's get this show on the road and implement
// our trait on this store
impl Repo for WidgetData {}
impl WidgetWithAssembly for WidgetData {}

// I know Fred said we should switch to a real database, but we
// just don't have the infra budget for that right now...
impl repox::FetchWithParentById<Widget, Assembly> for WidgetData {
    async fn exec(&self, id: u32) -> Result<
        (Widget, Assembly),
        repox::FetchWithParentError<Widget>,
    > {
        let Some(widget) = self.widgets.get(&id) else {
            return Err(repox::FetchWithParentError::NotFound(id));
        };
        let Some(assembly) = self.assemblies.get(&widget.assembly_id) else {
            return Err(repox::FetchWithParentError::NotFound(id));
        };
        Ok((widget.clone(), assembly.clone()))
    }
}

// prepare the data, so that it might be used for the tests to come
let data = WidgetData::default();
let cake = Widget {
    id: 1,
    assembly_id: 2_605_927_472,
    name: "Cake".into(),
};
let food = Assembly {
    id: 2_605_927_472,
    data: vec![0xF0, 0x0D],
};
data.widgets.insert(cake.id, cake.clone());
data.assemblies.insert(food.id, food.clone());

// and now... the moment of truth!  Fetch a widget
// with its parent assembly by ID!
pollster::block_on(async {
let (widget, assembly) = data.fetch_with_parent_by_id(1).await.unwrap();
assert_eq!(widget, cake);
assert_eq!(assembly, food);

// That's hot πŸ”₯
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
#[belongs_to(Widget, widget_id)]
pub struct Doodad {
    pub id: u32,
    pub widget_id: u32,
}

#[repox::mockall]
pub trait ParentRepo: Repo + repox::FetchWithParentById<Doodad, Widget> {}

let mut repo = MockParentRepo::new();
repo.expect_fetch_with_parent_by_id::<Doodad, Widget>()
    .withf(|id| *id == 67)
    .returning(repox::mock::ok_with(|id: u32| (
        Doodad { id, widget_id: 18 },
        Widget { id: 18, name: "WindSail".into() },
    )));


pollster::block_on(async {
let (doodad, widget) =
    repo.fetch_with_parent_by_id::<Doodad, Widget>(67).await.unwrap();
assert_eq!(doodad, Doodad { id: 67, widget_id: 18 });
assert_eq!(widget, Widget { id: 18, name: "WindSail".into() });
});

// Simply Fantastic ✨

insert

πŸ“ Method

insert(entity: Entity) -> Result<(), repox::InsertError<Entity>>

This method allows you to insert a new entity into the repository. It expects an instance of the entity you want to insert and returns Ok(()) on success. Your repository must implement the trait of Insert<T> for every entity type T you want to be able to insert.

🧩 Detailed Example:

Bob just came back from a big tech conference and is stoked on β€œsharding our widgets for webscale.” The ids are going to be generated by some external service, so we need to now be able to support inserting new widgets into our data store. It’s only a matter of time before we need to support this so let’s do it now!

use repox::Repo;

// Our new "webscale" widget
#[derive(Debug, Clone, PartialEq, repox::Entity)]
pub struct Widget {
    pub id: u128,
    pub name: std::sync::Arc<str>,
}

// We need to be able to insert these widgets into our data store
pub trait InsertableWidgetRepo: Repo + repox::Insert<Widget> {}

// Our new high-end data store that only works with widgets
#[derive(Debug, Default)]
pub struct WidgetData {
    pub data: dashmap::DashMap<u128, Widget>,
}

// look how proactive we are by implementing the Repo trait
impl Repo for WidgetData {}
impl InsertableWidgetRepo for WidgetData {}

// this is where it get's serious, stop goofing around Ben!
impl repox::Insert<Widget> for WidgetData {
    async fn exec(&self, widget: Widget)
    -> Result<(), repox::InsertError<Widget>>
    {
        self.data.insert(widget.id, widget);
        Ok(())
    }
}

// The guys back at the lab are never going to believe this, we
// just implemented an insert method for our widget repo!  Better
// Wire up a test to make sure it works and we can show it off to
// the team!
pollster::block_on(async {
let data = WidgetData::default();
let widget = Widget {
    id: 2_605_927_472,
    name: "QuantumTunnelVortex".into(),
};

// we're taking a quantum leap here by inserting this into our repo
data.insert(widget.clone()).await.unwrap();

// make sure it actually got in there, we don't want to look like fools
assert_eq!(data.data.get(&widget.id).unwrap().clone(), widget);

// Cowabunga! πŸ₯·πŸ’
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[repox::mockall]
pub trait WidgetInsert: Repo + repox::Insert<Widget> {}

let mut repo = MockWidgetInsert::new();
repo.expect_insert::<Widget>()
    .withf(|widget|{
        *widget == Widget { id: 7, name: "Sproket".into() }
    })
    .returning(repox::mock::ok_val(()));

pollster::block_on(async {
repo.insert(Widget { id: 7, name: "Sproket".into() } ).await.unwrap();
});

// Pretty cool 🧊

update_by_id

πŸ“ Method

update_by_id(entity: Entity) -> Result<(), repox::UpdateError<Entity>>

This method allows you to update an existing entity in the repository using the exact structure of the entity itself. The data store will use the unique identifier of the entity to find the existing record and update it with the new data provided in the entity. This is available when a repository implements the UpdateById<T> trait for the entity type T you want to support.

🧩 Detailed Example:

Old man Ferguson just came back from from the assembly line pissed off that his widgets are getting messed up in transit. He said we’ll have a β€œbig problem” if we don’t figure out how to update these defective widgets. Hopefully this update_by_id method is the solution to his problems.

use repox::Repo;

// our trusty ole widget entity, but now we
// need to be able to update it by ID
#[derive(Debug, Clone, PartialEq, repox::Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

// some of these widgets are getting messed up
// in transit, we need to be able to update them by ID
pub trait WidgetRepo: Repo + repox::UpdateById<Widget> {}

// Our new high-end data store that only works with widgets
#[derive(Debug, Default)]
pub struct WidgetData {
    pub data: dashmap::DashMap<u32, Widget>,
}

// We've got our top AI specialist Claude to help us
// out with this one, it's ready to give us edits for
// the defective widgets, so let's implement the trait
// and make sure it's working for us:
impl Repo for WidgetData {}
impl WidgetRepo for WidgetData {}

// this is the trait we need so we can update by id
impl repox::UpdateById<Widget> for WidgetData {
    async fn exec(&self, widget: Widget)
    -> Result<(), repox::UpdateError<Widget>>
    {
        if !self.data.contains_key(&widget.id) {
            return Err(repox::UpdateError::NotFound(widget.id));
        }
        self.data.insert(widget.id, widget);
        Ok(())
    }
}

// This is for all the marbles, we just implemented
// an update by ID method for our widget repo!  You
// know what to do, wire up a test to make sure it
// works and we can show it off to old man Ferguson!
pollster::block_on(async {
let repo = WidgetData::default();
let mut widget = Widget {
    id: 42,
    name: "BeeSauce".into(),
};
repo.data.insert(widget.id, widget.clone());

// Can you imagine the look on the shocked customers faces?
// I doubt Bee and Beef sauce are going to be all that similar.
widget.name.clear();
widget.name.push_str("BeefSauce");
repo.update_by_id(widget.clone()).await.unwrap();

// And now the big reveal, let's make sure it actually updated
let hopefully_updated = repo.data.get(&widget.id).unwrap().clone();
assert_eq!(hopefully_updated, widget);

// It's alive! πŸ§Ÿβ€β™‚οΈ
});

πŸ§ͺ Mock Example:

use repox::{Repo, Entity};

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[repox::mockall]
pub trait Updater: Repo + repox::UpdateById<Widget> {}

let mut repo = MockUpdater::new();
repo.expect_update_by_id::<Widget>()
    .withf(|widget|{
        *widget == Widget { id: 3, name: "DooHickey".into() }
    })
    .returning(repox::mock::ok_val(()));

pollster::block_on(async {
repo.update_by_id(Widget { id: 3, name: "DooHickey".into() })
    .await
    .unwrap();
});

// Power Overwhelming πŸ‘Ύ

πŸ”Ž repox::Entity Interface Methods

Introduction

repox::Entity is responsible for providing abstract functionality to data that is stored in a repository. It is a simple trait that requires an id from the identity trait, but it also provides a number of default methods. These can aid library maintainers in providing a consistent interface for different data back-ends.

belongs_to_key

πŸ“ Method

belongs_to_key<T: Entity>(&self) -> T::ID

Extracts the foreign key value from the entity that references its parent entity. This method is used in conjunction with the #[belongs_to] attribute to identify which field in the entity struct serves as the foreign key linking it to its parent entity. The method returns the value of this foreign key, which is typically the unique identifier of the parent entity. This allows you to easily access the parent entity’s ID when working with child entities that belong to it.

🧩 Detailed Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
#[belongs_to(Widget, widget_id)]
pub struct WidgetProvision {
    pub id: u128,
    pub widget_id: u32,
    pub float: f64,
}

let provision = WidgetProvision { id: 2, widget_id: 42, float: 1.337 };

assert_eq!(provision.belongs_to_key::<Widget>(), 42);

belongs_to

πŸ“ Method

belongs_to<T: Entity>(&self, entity: &T) -> bool

A convenience method that checks if the entity belongs to a specific parent entity. This method is used in conjunction with the #[belongs_to] attribute to determine if the child entity is associated with the given parent entity. It works by comparing the foreign key value extracted from the child entity (using belongs_to_key) with the unique identifier of the provided parent entity. If they match, it returns true, indicating that the child entity belongs to the specified parent; otherwise, it returns false.

🧩 Detailed Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
#[belongs_to(Widget, widget_id)]
pub struct WidgetProvision {
    pub id: u128,
    pub widget_id: u32,
    pub float: f64,
}

let provision = WidgetProvision { id: 2, widget_id: 42, float: 1.337 };
let foo = Widget { id: 42, name: "foo".into() };
let bar = Widget { id: 13, name: "bar".into() };

assert!(provision.belongs_to(&foo));
assert!(!provision.belongs_to(&bar));

has_many_key

πŸ“ Method

has_many_key<T: Entity>(&self, entity: &T) -> Self::ID

Extracts the foreign key value from the entity that references the current entity as its parent. This method is used in conjunction with the #[has_many] attribute to identify which field in the child entity structure and field type serves as the foreign key linking it to the parent entity. This allows you to easily access the parent entity’s ID when working with entities without needing to know the specific field name of the foreign key.

🧩 Detailed Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[has_many(WidgetProvision.widget_id)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct WidgetProvision {
    pub id: u128,
    pub widget_id: u32,
    pub float: f64,
}

let foo = Widget { id: 1, name: "foo".into() };
let provision = WidgetProvision { id: 2, widget_id: 42, float: 1.337 };

assert_eq!(foo.has_many_key::<WidgetProvision>(&provision), 42);

is_owner_of

πŸ“ Method

is_owner_of<T: Entity>(&self, entity: &T) -> bool

Convenience method that checks if the entity is the owner of a specific child

🧩 Detailed Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[has_many(WidgetProvision.widget_id)]
pub struct Widget {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct WidgetProvision {
    pub id: u128,
    pub widget_id: u32,
    pub float: f64,
}

let foo = Widget { id: 42, name: "foo".into() };
let bar = Widget { id: 13, name: "bar".into() };
let lucky_val = WidgetProvision { id: 2, widget_id: 42, float: 1.337 };

assert!(foo.is_owner_of(&lucky_val));
assert!(!bar.is_owner_of(&lucky_val));

repox::Entity Derive Attributes

Introduction

repox::Entity is both a trait and a derive macro for the repox framework. The Entity trait itself is very simple, requiring only an id method that returns the entity’s unique identifier. However, the Entity derive macro provides tags which implement additional functionality for the entity.

#belongs_to

🏭 Macro

#[belongs_to($target_type, $field_name)]

Implements a belongs-to relationship between the current entity and the target entity. The $target_type is another entity which this entity belongs to. $field_name is the field on this structure which implements the relationship. This field must be of the same type as the $target_type’s ID.

πŸ“ Macro Example:

This is a short example of how to use the #belongs_to macro. Here we have Post which belongs to Author via the author_id field.

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Author {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
#[belongs_to(Author, author_id)] // <- Post belongs to Author via author_id
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// Quick Demo of the relationship in action:
let alice = Author { id: 1, name: "Alice".to_string() };
let darin = Author { id: 2, name: "Darin".to_string() };
let post = Post { id: 100, author_id: 1, data: "Hello World".to_string() };
assert!(post.belongs_to(&alice));
assert!(!post.belongs_to(&darin));
assert_eq!(post.belongs_to_key::<Author>(), 1);

πŸ”¬ Macro Details:

Here is the same example, but, without using the macro and implementing it ourselves. This highlights the fact that this macro is just a convenient way to implement the BelongsToForeignKey trait and if you have a more complex relationship, you can implement it yourself without the macro. There is no magic here πŸ§™β€β™‚οΈ!

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Author {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

impl ::repox::BelongsToForeignKey<Author> for Post {
    fn key(&self) -> <Author as ::repox::Identity>::ID {
        self.author_id
    }
}

// Same Demo of the relationship in action:
let alice = Author { id: 1, name: "Alice".to_string() };
let darin = Author { id: 2, name: "Darin".to_string() };
let post = Post { id: 100, author_id: 1, data: "Hello World".to_string() };
assert!(post.belongs_to(&alice));
assert!(!post.belongs_to(&darin));
assert_eq!(post.belongs_to_key::<Author>(), 1);

Note: Still deriving Entity to avoid extra boilerplate above.

#create_params

🏭 Macro

#[create_params($struct_name, $op(...) = excluding_id())]

This macro generates a new struct which can be used as parameters for creating a new entity. The generated struct’s fields are the same as the entity but without the ID field by default. The following table describes the operations that can be used in the macro:

operationdescription
all()includes all fields from entity (including ID)
excluding_id()includes all fields except the ID field
only(…)will only contain these fields
excluding(…)includes all fields except these fields

πŸ“ Macro Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(PostParams)] // <- create with default `excluding_id()`
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// It can be used by repositories to create new entities
async fn create_sample_post(
    repository: &impl repox::CreateWith<Post, PostParams>,
    author_id: u32
) -> Result<Post, anyhow::Error> {
    repository.create_with(PostParams { // <- missing `id` field since
        author_id,                      // we used `excluding_id()`
        data: "Sample Post".into(),
    }).await
}

πŸ”¬ Macro Details:

Here is the same example, but, without using the macro and implementing it ourselves. This highlights the fact that this macro is just a convenient way to implement the CreateWith<T, P> trait for a freshly minted structure with the same fields as the entity, but without the ID field by default. There is no magic here πŸ§™β€β™‚οΈ!

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// This is the exact same structure the macro above would generate
#[derive(Debug, Clone)]
pub struct PostParams {
    pub author_id: u32,
    pub data: String,
}

// Parameters need to implement `Creatable` for the entity
// they are compatible with.  For now this trait is empty,
// but in the future it may contain some useful functionality.
impl ::repox::Creatable<Post> for PostParams {}

// It can be used by repositories to create new entities
async fn create_sample_post(
    repository: &impl repox::CreateWith<Post, PostParams>,
    author_id: u32
) -> Result<Post, anyhow::Error> {
    repository.create_with(PostParams { // <- missing `id` field since
        author_id,                      // we used `excluding_id()`
        data: "Sample Post".into(),
    }).await
}

all()

πŸ“ Macro Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(PostParams, all())]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// All of the fields are included
let post = PostParams { id: 100, author_id: 1, data: "Hello World".into() };

excluding(...)

πŸ“ Macro Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(PostPlaceholder, excluding(id, data))]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// only has `author_id` since we excluded `id` and `data`
let post = PostPlaceholder { author_id: 1 };

only(...)

πŸ“ Macro Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[create_params(AnonPostData, only(data))]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// only has `data` field
let post = AnonPostData { data: "You too can example.".into() };

#created_by

🏭 Macro

#[created_by($struct_type)]

This macro is a simple way to implement the Creatable<T> trait for a custom structure you want to use as parameters when creating a new entity. Today, this macro is just a convenient way to implement the Creatable<T>; however, in the future, it may be extended to support more features which are common for creation parameters.

πŸ“ Macro Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[created_by(PostParams)]  // <- this structure can be used to create new
pub struct Post {          //    Post entities for supporting repositories
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

#[derive(Debug, Clone)]
pub struct PostParams {
    pub author_id: u32,
    pub title: String,      // <- it will be on the repository to decide
    pub content: String,    //    how to use these fields to create a Post
}

// It can be used by repositories to create new entities
async fn create_sample_post(
    repository: &impl repox::CreateWith<Post, PostParams>,
    author_id: u32
) -> Result<Post, anyhow::Error> {
    repository.create_with(PostParams {
        author_id,
        title: "Sample Title".into(),
        content: "Sample content...".into(),
    }).await
}

πŸ”¬ Macro Details:

Here is the same example, but, without using the macro to show that there is no magic here and that this macro is just a convenient way to implement the Creatable<T> trait for a custom structure πŸ§™β€β™‚οΈ.

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

#[derive(Debug, Clone)]
pub struct PostParams {
    pub author_id: u32,
    pub title: String,
    pub content: String,
}

// without the following impl the `create_sample_post`
// would not compile since `CreateWith<T, P>` requires
// that `P` be `Creatable<T>`
impl repox::Creatable<Post> for PostParams {}

// It can be used by repositories to create new entities
async fn create_sample_post(
    repository: &impl repox::CreateWith<Post, PostParams>,
    author_id: u32
) -> Result<Post, anyhow::Error> {
    repository.create_with(PostParams {
        author_id,
        title: "Sample Title".into(),
        content: "Sample content...".into(),
    }).await
}

#custom_id

🏭 Macro

#[custom_id($id_type, $func_path)]

At the heart of the entity concept is the Identifier trait. Every entity must have an identifier, and the Identifier trait defines what type that is and a method of id(&self) to retrieve it. By default the Entity macro selects a field named id; however, this macro is a convenient shortcut to specify a custom function that can implement the Identifier trait for you. The first argument is the ID type, and the second is a path to a function that takes a reference to the entity and returns the ID.

πŸ“ Macro Example:

In this example, we have a UserRole entity which has a composite key of UserRoleID consisting of user_id and role_id. We use the defined From<&UserRole> implementation to convert a &UserRole into a UserRoleID and specify that as the custom ID function in the macro.

use repox::{Entity, Identity};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, Hash, PartialOrd)]
pub struct UserRoleID {
    pub user_id: u32,
    pub role_id: u32,
}

#[derive(Debug, Clone, PartialEq, Entity)]
#[custom_id(UserRoleID, UserRoleID::from)]
pub struct UserRole {
    pub user_id: u32,
    pub role_id: u32,
    pub created_at: u64,
}

impl From<&UserRole> for UserRoleID {
    fn from(user_role: &UserRole) -> Self {
        Self { user_id: user_role.user_id, role_id: user_role.role_id }
    }
}

// Quick Demo of the custom ID in action:
let user_role = UserRole { user_id: 1, role_id: 2, created_at: 123456789 };
assert_eq!(user_role.id(), UserRoleID { user_id: 1, role_id: 2 });

πŸ”¬ Macro Details:

Here is the same example, but, without using the macro and implementing it ourselves.

use repox::{Entity, Identity};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, Hash, PartialOrd)]
pub struct UserRoleID {
    pub user_id: u32,
    pub role_id: u32,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct UserRole {
    pub user_id: u32,
    pub role_id: u32,
    pub created_at: u64,
}

impl From<&UserRole> for UserRoleID {
    fn from(user_role: &UserRole) -> Self {
        Self { user_id: user_role.user_id, role_id: user_role.role_id }
    }
}

// this is what the `custom_id` macro generates for us
impl Identity for UserRole {
    type ID = UserRoleID;
    fn id(&self) -> Self::ID { UserRoleID::from(self) }
}

// Quick Demo of the custom ID in action:
let user_role = UserRole { user_id: 1, role_id: 2, created_at: 123456789 };
assert_eq!(user_role.id(), UserRoleID { user_id: 1, role_id: 2 });

#entity

🏭 Macro

#[entity(id)]

Provides a quick way to identify a custom field as the ID for an entity.

πŸ“ Macro Example:

use repox::{Entity, Identity};

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct UserPasswordHash {
    #[entity(id)]
    pub user_id: u32,
    pub data: Vec<u8>,
}

// Quick Demo of the ID in action:
let hash = UserPasswordHash { user_id: 42, data: vec![1, 2, 3] };
assert_eq!(hash.id(), 42);

πŸ”¬ Macro Details:

Here is the same example, but, without using the macro. πŸ§™β€β™‚οΈ

use repox::{Entity, Identity};

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct UserPasswordHash {
    pub user_id: u32,
    pub data: Vec<u8>,
}

// this is what the `entity(id)` macro generates for us
impl Identity for UserPasswordHash {
    type ID = u32;
    fn id(&self) -> Self::ID { self.user_id }
}

// Quick Demo of the ID in action:
let hash = UserPasswordHash { user_id: 67, data: vec![1, 2, 3] };
assert_eq!(hash.id(), 67);

#has_many

🏭 Macro

#[has_many($struct_name.$struct_foreign_key)]

Implements a has-many relationship between the current entity and another entity. The $struct_name is the name of the other entity and the $struct_foreign_key is the field on that entity which should equal the ID of this entity. This macro is just a convenient way to set up the relationship by implementing the HasManyForeignKey.

πŸ“ Macro Example:

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
#[has_many(Post.author_id)]   // <- Author has many Posts via Post.author_id
pub struct Author {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// Quick Demo of the relationship in action:
let alice = Author { id: 1, name: "Alice".to_string() };
let darin = Author { id: 2, name: "Darin".to_string() };
let post = Post { id: 100, author_id: 1, data: "Hello World".to_string() };
assert!(alice.is_owner_of(&post));
assert!(!darin.is_owner_of(&post));
assert_eq!(alice.has_many_key::<Post>(&post), 1);

πŸ”¬ Macro Details:

Here is the same example, but, without using the macro. You can see that it’s just a convenient way to set up the relationship by implementing HasManyForeignKey for you.

use repox::Entity;

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Author {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Entity)]
pub struct Post {
    pub id: u64,
    pub author_id: u32,
    pub data: String,
}

// this is what the #[has_many(Post.author_id)] macro expands to:
impl repox::HasManyForeignKey<Post> for Author {
    fn key(entity: &Post) -> <Author as repox::Identity>::ID {
        entity.author_id
    }
}

// Quick Demo of the relationship in action:
let alice = Author { id: 1, name: "Alice".to_string() };
let darin = Author { id: 2, name: "Darin".to_string() };
let post = Post { id: 100, author_id: 1, data: "Hello World".to_string() };
assert!(alice.is_owner_of(&post));
assert!(!darin.is_owner_of(&post));
assert_eq!(alice.has_many_key::<Post>(&post), 1);

πŸ”­ Project Overview

A fairly vanilla rust library setup. In case its not, or you just want a high level tour, here it is!

πŸ—‚οΈ Directory Structure

── .github ------------------ CI workflow defintions
   crates ------------------- directory for all workspace crates
   β”œβ”€β”€ repox  --------------- main library crate
       β”œβ”€β”€ doc -------------- mdbook and crate documentation source
       β”œβ”€β”€ src -------------- crate source code
       β”œβ”€β”€ theme ------------ asset dir for mdbook
   β”œβ”€β”€ repox_derive --------- supporting proc macro crate
       β”œβ”€β”€ src -------------- crate source code
   β”œβ”€β”€ showtime ------------- example binary with sqlx and sqlite
       β”œβ”€β”€ migrations ------- sqlx migrations
       β”œβ”€β”€ src -------------- crate source code