diff --git a/Cargo.lock b/Cargo.lock index 536be74..aa26687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2202,7 +2202,7 @@ dependencies = [ "hex", "miette", "models 0.1.0", - "repos", + "repos 0.1.0 (git+https://github.com/rambit-systems/rambit)", "thiserror 2.0.3", "tracing", ] @@ -2600,6 +2600,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "repos" +version = "0.1.0" +dependencies = [ + "async-trait", + "db", + "hex", + "miette", + "models 0.1.0", + "tracing", +] + [[package]] name = "repos" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6b947df..7c77830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ thiserror = "2" # inherited from rambit crunch = { git = "https://github.com/rambit-systems/rambit" } +db = { git = "https://github.com/rambit-systems/rambit" } dvf = { git = "https://github.com/rambit-systems/rambit" } hex = { git = "https://github.com/rambit-systems/rambit" } model = { git = "https://github.com/rambit-systems/rambit" } diff --git a/crates/repos/Cargo.toml b/crates/repos/Cargo.toml new file mode 100644 index 0000000..b5caabd --- /dev/null +++ b/crates/repos/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "repos" +version = "0.1.0" +edition = "2021" + +[dependencies] +models = { path = "../models" } + +db.workspace = true +hex.workspace = true + +tracing.workspace = true + +miette.workspace = true + +async-trait.workspace = true + +[lints] +workspace = true diff --git a/crates/repos/src/base.rs b/crates/repos/src/base.rs new file mode 100644 index 0000000..dae2b37 --- /dev/null +++ b/crates/repos/src/base.rs @@ -0,0 +1,97 @@ +use std::marker::PhantomData; + +pub use db::CreateModelError; +pub(crate) use db::{DatabaseAdapter, FetchModelByIndexError, FetchModelError}; +use hex::health; +use miette::Result; +use tracing::instrument; + +use crate::ModelRepository; + +/// Provides a base repository implementation for any model. +pub struct BaseRepository { + db_adapter: DB, + _phantom: PhantomData, +} + +impl Clone + for BaseRepository +{ + fn clone(&self) -> Self { + Self { + db_adapter: self.db_adapter.clone(), + _phantom: PhantomData, + } + } +} + +impl BaseRepository { + /// Creates a new `BaseRepository` instance. + pub fn new(db_adapter: DB) -> Self { + tracing::info!( + "creating new `BaseRepository<{:?}>` instance", + M::TABLE_NAME + ); + + Self { + db_adapter, + _phantom: PhantomData, + } + } +} + +#[async_trait::async_trait] +impl health::HealthReporter + for BaseRepository +{ + fn name(&self) -> &'static str { stringify!(BaseRepository) } + async fn health_check(&self) -> health::ComponentHealth { + health::AdditiveComponentHealth::from_futures(Some( + self.db_adapter.health_report(), + )) + .await + .into() + } +} + +#[async_trait::async_trait] +impl ModelRepository + for BaseRepository +{ + type Model = M; + type ModelCreateRequest = M; + type CreateError = CreateModelError; + + #[instrument(skip(self))] + async fn create_model( + &self, + input: Self::ModelCreateRequest, + ) -> Result { + self.db_adapter.create_model::(input).await + } + + #[instrument(skip(self))] + async fn fetch_model_by_id( + &self, + id: models::RecordId, + ) -> Result, FetchModelError> { + self.db_adapter.fetch_model_by_id(id).await + } + + #[instrument(skip(self))] + async fn fetch_model_by_index( + &self, + index_name: String, + index_value: models::EitherSlug, + ) -> Result, FetchModelByIndexError> { + self + .db_adapter + .fetch_model_by_index(index_name, index_value) + .await + } + + #[instrument(skip(self))] + async fn enumerate_models(&self) -> Result> { + self.db_adapter.enumerate_models::().await + } +} diff --git a/crates/repos/src/lib.rs b/crates/repos/src/lib.rs new file mode 100644 index 0000000..951e640 --- /dev/null +++ b/crates/repos/src/lib.rs @@ -0,0 +1,78 @@ +//! Repositories for use in services. + +use db::{FetchModelByIndexError, FetchModelError}; +use hex::Hexagonal; +use miette::Result; +use models::EitherSlug; + +mod base; +pub use self::base::BaseRepository; + +/// Defines a repository interface for models. +#[async_trait::async_trait] +pub trait ModelRepository: Hexagonal { + /// The model type. + type Model: models::Model; + /// The request type for creating a model. + type ModelCreateRequest: std::fmt::Debug + Send + Sync + 'static; + /// The error type for creating a model. + type CreateError: std::error::Error + Send + Sync + 'static; + + /// Creates a new model. + async fn create_model( + &self, + input: Self::ModelCreateRequest, + ) -> Result; + + /// Fetches a model by its ID. + async fn fetch_model_by_id( + &self, + id: models::RecordId, + ) -> Result, FetchModelError>; + + /// Fetches a model by an index. + /// + /// Must be a valid index, defined in the model's `INDICES` constant. + async fn fetch_model_by_index( + &self, + index_name: String, + index_value: EitherSlug, + ) -> Result, FetchModelByIndexError>; + + /// Produces a list of all model IDs. + async fn enumerate_models(&self) -> Result>; +} + +#[async_trait::async_trait] +impl ModelRepository for T +where + T: std::ops::Deref + Hexagonal + Sized, + I: ModelRepository + ?Sized, +{ + type Model = I::Model; + type ModelCreateRequest = I::ModelCreateRequest; + type CreateError = I::CreateError; + + async fn create_model( + &self, + input: Self::ModelCreateRequest, + ) -> Result { + I::create_model(self, input).await + } + async fn fetch_model_by_id( + &self, + id: models::RecordId, + ) -> Result, FetchModelError> { + I::fetch_model_by_id(self, id).await + } + async fn fetch_model_by_index( + &self, + index_name: String, + index_value: EitherSlug, + ) -> Result, FetchModelByIndexError> { + I::fetch_model_by_index(self, index_name, index_value).await + } + async fn enumerate_models(&self) -> Result> { + I::enumerate_models(self).await + } +}