Skip to content

Commit

Permalink
implement contains_key, update, update_or
Browse files Browse the repository at this point in the history
  • Loading branch information
keithamus committed Aug 20, 2024
1 parent 48646d1 commit d1639b9
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 0 deletions.
4 changes: 4 additions & 0 deletions actix-session/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Add `session.update(key, updater)`
- Add `session.update_or(key, updater, default_value)`
- Add `session.contains_key(key)`

## 0.10.0

- Add `redis-session-rustls` crate feature that enables `rustls`-secured Redis sessions.
Expand Down
91 changes: 91 additions & 0 deletions actix-session/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ use serde::{de::DeserializeOwned, Serialize};
/// } else {
/// session.insert("counter", 1)?;
/// }
/// // Or use the shorthand
/// session.update_or("counter", |count: i32| count + 1, 1);
///
/// Ok("Welcome!")
/// }
Expand Down Expand Up @@ -97,6 +99,13 @@ impl Session {
}
}

/// Check if a key exists on the session.
///
/// It returns true if the session contains a vaue for the specified key.
pub fn contains_key(&self, key: &str) -> bool {
self.0.borrow().state.contains_key(key)
}

/// Get all raw key-value data from the session.
///
/// Note that values are JSON encoded.
Expand Down Expand Up @@ -145,6 +154,83 @@ impl Session {
Ok(())
}

/// Updates a key-value pair into the session.
///
/// If the key exists then update it to the new value and place it back in.
///
/// If the key does not exist it will not be updated.
///
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
/// only a reference to the value is taken.
///
/// It returns an error if it fails to serialize `value` to JSON.
pub fn update<T: Serialize + DeserializeOwned, F>(
&self,
key: impl Into<String>,
updater: F,
) -> Result<(), SessionUpdateError>
where
F: FnOnce(T) -> T,
{
let mut inner = self.0.borrow_mut();
let key_str = key.into();

if let Some(val_str) = inner.state.get(&key_str) {
let value = serde_json::from_str(val_str)
.with_context(|| {
format!(
"Failed to deserialize the JSON-encoded session data attached to key \
`{}` as a `{}` type",
key_str,
std::any::type_name::<T>()
)
})
.map_err(SessionUpdateError)?;

let val = serde_json::to_string(&updater(value))
.with_context(|| {
format!(
"Failed to serialize the provided `{}` type instance as JSON in order to \
attach as session data to the `{}` key",
std::any::type_name::<T>(),
key_str
)
})
.map_err(SessionUpdateError)?;

inner.state.insert(key_str, val);
}

Ok(())
}

/// Updates a key-value pair into the session, or inserts a default value.
///
/// If the key exists then update it to the new value and place it back in.
///
/// If the key does not exist the default value will be inserted instead.
///
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
/// only a reference to the value is taken.
///
/// It returns an error if it fails to serialize `value` to JSON.
pub fn update_or<T: Serialize + DeserializeOwned, F>(
&self,
key: &str,
updater: F,
default_value: T,
) -> Result<(), SessionUpdateError>
where
F: FnOnce(T) -> T,
{
if self.contains_key(key) {
self.update(key, updater)
} else {
self.insert(key, default_value)
.map_err(|e| SessionUpdateError(e.into()))
}
}

/// Remove value from the session.
///
/// If present, the JSON encoded value is returned.
Expand Down Expand Up @@ -308,6 +394,11 @@ impl ResponseError for SessionGetError {
#[display("{_0}")]
pub struct SessionInsertError(anyhow::Error);

/// Error returned by [`Session::update`].
#[derive(Debug, Display, From)]
#[display("{_0}")]
pub struct SessionUpdateError(anyhow::Error);

impl StdError for SessionInsertError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.0.as_ref())
Expand Down
39 changes: 39 additions & 0 deletions actix-session/tests/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ async fn session_entries() {
map.contains_key("test_num");
}

#[actix_web::test]
async fn session_contains_key() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
session.insert("test_str", "val").unwrap();
session.insert("test_str", 1).unwrap();
assert!(session.contains_key("test_str"));
assert!(!session.contains_key("test_num"));
}

#[actix_web::test]
async fn insert_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session();
Expand All @@ -83,6 +93,35 @@ async fn insert_session_after_renew() {
assert_eq!(session.status(), SessionStatus::Renewed);
}

#[actix_web::test]
async fn update_session() {
let session = test::TestRequest::default().to_srv_request().get_session();

session.update("test_val", |c: u32| c + 1).unwrap();
assert_eq!(session.status(), SessionStatus::Unchanged);

session.insert("test_val", 0).unwrap();
assert_eq!(session.status(), SessionStatus::Changed);

session.update("test_val", |c: u32| c + 1).unwrap();
assert_eq!(session.get("test_val").unwrap(), Some(1));

session.update("test_val", |c: u32| c + 1).unwrap();
assert_eq!(session.get("test_val").unwrap(), Some(2));
}

#[actix_web::test]
async fn update_or_session() {
let session = test::TestRequest::default().to_srv_request().get_session();

session.update_or("test_val", |c: u32| c + 1, 1).unwrap();
assert_eq!(session.status(), SessionStatus::Changed);
assert_eq!(session.get("test_val").unwrap(), Some(1));

session.update_or("test_val", |c: u32| c + 1, 1).unwrap();
assert_eq!(session.get("test_val").unwrap(), Some(2));
}

#[actix_web::test]
async fn remove_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session();
Expand Down

0 comments on commit d1639b9

Please sign in to comment.