-
-
Notifications
You must be signed in to change notification settings - Fork 296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add get_or_insert function to cache layer #673
Changes from all commits
55acba3
0c6f5dd
983f67e
fd833e8
d4a2fcc
b89e141
89cf0dd
790c8be
8cada1a
dda52a3
419f36a
c9cb135
5bd04d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
use crate::models::users; | ||
use loco_rs::prelude::*; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct CacheResponse { | ||
value: Option<String>, | ||
|
@@ -11,14 +11,31 @@ async fn get_cache(State(ctx): State<AppContext>) -> Result<Response> { | |
value: ctx.cache.get("value").await.unwrap(), | ||
}) | ||
} | ||
|
||
async fn insert(State(ctx): State<AppContext>) -> Result<Response> { | ||
ctx.cache.insert("value", "loco cache value").await.unwrap(); | ||
format::empty() | ||
} | ||
|
||
async fn get_or_insert(State(ctx): State<AppContext>) -> Result<Response> { | ||
let res = ctx | ||
.cache | ||
.get_or_insert("user", async { | ||
let user = users::Model::find_by_email(&ctx.db, "[email protected]").await?; | ||
Ok(user.name) | ||
}) | ||
.await; | ||
|
||
match res { | ||
Ok(username) => format::text(&username), | ||
Err(_e) => format::text("not found"), | ||
} | ||
} | ||
|
||
pub fn routes() -> Routes { | ||
Routes::new() | ||
.prefix("cache") | ||
.add("/", get(get_cache)) | ||
.add("/insert", post(insert)) | ||
.add("/get_or_insert", get(get_or_insert)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
use blo::app::App; | ||
use blo::{app::App, models::users}; | ||
use insta::assert_debug_snapshot; | ||
use loco_rs::testing; | ||
use sea_orm::ModelTrait; | ||
use serial_test::serial; | ||
|
||
// TODO: see how to dedup / extract this to app-local test utils | ||
// not to framework, because that would require a runtime dep on insta | ||
|
@@ -14,6 +16,7 @@ macro_rules! configure_insta { | |
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn ping() { | ||
configure_insta!(); | ||
|
||
|
@@ -27,3 +30,23 @@ async fn ping() { | |
}) | ||
.await; | ||
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn can_get_or_insert() { | ||
configure_insta!(); | ||
|
||
testing::request::<App, _, _>(|request, ctx| async move { | ||
testing::seed::<App>(&ctx.db).await.unwrap(); | ||
let response = request.get("/cache/get_or_insert").await; | ||
assert_eq!(response.text(), "user1"); | ||
|
||
let user = users::Model::find_by_email(&ctx.db, "[email protected]") | ||
.await | ||
.unwrap(); | ||
user.delete(&ctx.db).await.unwrap(); | ||
let response = request.get("/cache/get_or_insert").await; | ||
assert_eq!(response.text(), "user1"); | ||
}) | ||
.await; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
mod auth; | ||
mod cache; | ||
pub mod mylayer; | ||
mod mylayer; | ||
mod notes; | ||
mod ping; | ||
mod prepare_data; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,10 @@ | |
//! This module provides a generic cache interface for various cache drivers. | ||
pub mod drivers; | ||
|
||
use std::future::Future; | ||
|
||
use self::drivers::CacheDriver; | ||
use crate::Result as LocoResult; | ||
|
||
/// Errors related to cache operations | ||
#[derive(thiserror::Error, Debug)] | ||
|
@@ -84,6 +87,40 @@ impl Cache { | |
self.driver.insert(key, value).await | ||
} | ||
|
||
/// Retrieves the value associated with the given key from the cache, | ||
/// or inserts it if it does not exist, using the provided closure to | ||
/// generate the value. | ||
/// | ||
/// # Example | ||
/// ``` | ||
/// use loco_rs::{app::AppContext}; | ||
/// use loco_rs::tests_cfg::app::*; | ||
/// | ||
/// pub async fn get_or_insert(){ | ||
/// let app_ctx = get_app_context().await; | ||
/// let res = app_ctx.cache.get_or_insert("key", async { | ||
/// Ok("value".to_string()) | ||
/// }).await.unwrap(); | ||
/// assert_eq!(res, "value"); | ||
/// } | ||
/// ``` | ||
/// | ||
/// # Errors | ||
/// | ||
/// A [`LocoResult`] indicating the success of the operation. | ||
pub async fn get_or_insert<F>(&self, key: &str, f: F) -> LocoResult<String> | ||
where | ||
F: Future<Output = LocoResult<String>> + Send, | ||
{ | ||
if let Some(value) = self.driver.get(key).await? { | ||
Ok(value) | ||
} else { | ||
let value = f.await?; | ||
self.driver.insert(key, &value).await?; | ||
Ok(value) | ||
} | ||
} | ||
|
||
/// Removes a key-value pair from the cache. | ||
/// | ||
/// # Example | ||
|
@@ -122,3 +159,29 @@ impl Cache { | |
self.driver.clear().await | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
|
||
use crate::tests_cfg; | ||
|
||
#[tokio::test] | ||
async fn can_get_or_insert() { | ||
let app_ctx = tests_cfg::app::get_app_context().await; | ||
let get_key = "loco"; | ||
|
||
assert_eq!(app_ctx.cache.get(get_key).await.unwrap(), None); | ||
|
||
let result = app_ctx | ||
.cache | ||
.get_or_insert(get_key, async { Ok("loco-cache-value".to_string()) }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you try a test where the async block captures something from app_ctx? like try a real DB call for example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added endpoint usage example + adding tests |
||
.await | ||
.unwrap(); | ||
|
||
assert_eq!(result, "loco-cache-value".to_string()); | ||
assert_eq!( | ||
app_ctx.cache.get(get_key).await.unwrap(), | ||
Some("loco-cache-value".to_string()) | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something still seems off to me w/errors.
i believe, async block should just throw the regular Result from Loco (i.e.
CacheError
does not belong in that async block)the block runs like any other "linear" code flow, only that in our case, the result of the block is cached. So the block can return a result or fail, like any other code flow. The failure should be for any original reason the block itself has failed.
a good way to look at it:
if you remove the cache completely, the code block should mostly stay the same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got you, changed for supporting it