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::Repois the main trait interface for therepoxframework. Basic CRUD1 operations are supported by repository traits, such asDeleteById<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.
-
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 parametersstruct. 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_withto 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 typeTyou 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. TheErrvariant will be aFetchErrorwhich 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_idmethod by implementing the [FetchById<T>] trait for ourWidgetDatarepository.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 typeTyou want to fetch. This method allows you to attempt to fetch an entity by its unique identifier, returningOk(Some(entity))if found,Ok(None)if not found, andErr(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 ofInsert<T>for every entity typeTyou 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 typeTyou 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_idmethod 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::Entityis responsible for providing abstract functionality to data that is stored in a repository. It is a simple trait that requires anidfrom 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::IDExtracts 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) -> boolA 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 (usingbelongs_to_key) with the unique identifier of the provided parent entity. If they match, it returnstrue, indicating that the child entity belongs to the specified parent; otherwise, it returnsfalse.π§© 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::IDExtracts 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) -> boolConvenience 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::Entityis both a trait and a derive macro for therepoxframework. TheEntitytrait itself is very simple, requiring only anidmethod that returns the entityβs unique identifier. However, theEntityderive 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_typeis another entity which this entity belongs to.$field_nameis 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_tomacro. Here we havePostwhich belongs toAuthorvia theauthor_idfield.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
BelongsToForeignKeytrait 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
Entityto 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:
operation description 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 theCreatable<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
Identifiertrait. Every entity must have an identifier, and theIdentifiertrait defines what type that is and a method ofid(&self)to retrieve it. By default theEntitymacro selects a field namedid; however, this macro is a convenient shortcut to specify a custom function that can implement theIdentifiertrait 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
UserRoleentity which has a composite key ofUserRoleIDconsisting ofuser_idandrole_id. We use the definedFrom<&UserRole>implementation to convert a&UserRoleinto aUserRoleIDand 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_nameis the name of the other entity and the$struct_foreign_keyis 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 theHasManyForeignKey.π 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
HasManyForeignKeyfor 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