Skip to content

Commit

Permalink
feat: add extensions field to AppState (#131)
Browse files Browse the repository at this point in the history
* added extensions field to AppState, including testing

* resolved suggestions Norlock

* resolved comments, moved extension testcode to unime/src-tauri/tests

* move import to dev-dependencies, remove accidental binding

* cargo fmt

* deleted png and svg files
  • Loading branch information
Oran-Dan authored Feb 26, 2024
1 parent aa9f673 commit 7b7e4ff
Show file tree
Hide file tree
Showing 17 changed files with 192 additions and 4 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions identity-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ thiserror = "1.0"
tokio = { version = "1.26.0", features = ["macros"] }
ts-rs = "7.0"
typetag = "0.2"
dyn-clone = "1.0"
uuid = { version = "1.4", features = ["v4", "fast-rng", "serde"] }

[dev-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions identity-wallet/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use log::{debug, error, info};
use std::time::Duration;
use tauri::Manager;

/// The command.rs holds the functions through which the front and backend communicate using actions and reducers.
/// This function represents the root reducer of the application. It will delegate the state update to the reducers that
/// are listening to the action.
pub(crate) async fn reduce(state: AppState, action: Action) -> Result<AppState, AppError> {
Expand All @@ -33,6 +35,7 @@ pub(crate) async fn reduce(state: AppState, action: Action) -> Result<AppState,
// This value is based on an estimated guess. Can be adjusted in case lower/higher timeouts are desired.
const TIMEOUT_SECS: u64 = 6;

/// This function is used to prevent deadlocks in the backend. It will sleep for a certain amount of time and then return.
async fn deadlock_safety() {
tokio::time::sleep(Duration::from_secs(TIMEOUT_SECS)).await;
}
Expand Down Expand Up @@ -95,6 +98,7 @@ pub async fn handle_action<R: tauri::Runtime>(
}
}

/// This function emits an event to the frontend with the updated state.
pub fn emit_event<R: tauri::Runtime>(window: &tauri::Window<R>, app_state: &AppState) -> anyhow::Result<()> {
const STATE_CHANGED_EVENT: &str = "state-changed";
window.emit(STATE_CHANGED_EVENT, app_state)?;
Expand Down
3 changes: 3 additions & 0 deletions identity-wallet/src/crypto/stronghold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use log::{debug, info};
use oid4vc::oid4vc_core::authentication::sign::ExternalSign;
use uuid::Uuid;

/// This file is where we implement the stronghold library for our app, which is used to store sensitive data.
/// This struct is the main point of communication between our appstate and the stronghold library.
#[derive(Debug)]
pub struct StrongholdManager {
stronghold: Stronghold,
Expand Down
3 changes: 2 additions & 1 deletion identity-wallet/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::state::actions::Action;
use oid4vc::oid4vc_core::authorization_request::{AuthorizationRequest, Object};
use std::error::Error;
use uuid::Uuid;

use crate::state::actions::Action;
/// The error.rs defines our app_error types, implemented throughout the code using the thiserror crate.
// TODO: needs revision/refactor + needs oid4vc libs to properly implement error handling.
#[derive(thiserror::Error)]
Expand Down
6 changes: 6 additions & 0 deletions identity-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ pub mod state;
pub mod utils;
pub mod verifiable_credential_record;

/// This folder is where the main backend rust code lives together with all the business logic.
/// The folder state is where our appstate and it's features are defined, completely according to the redux pattern.
/// The command.rs holds the functions through which the front and backend comminicate using actions and reducers.
/// The error.rs defines our app_error types, implemented throughout the code using the thiserror crate.
/// The persistence.rs is where we define our app persistence functions.
/// The stronghold.rs is where we implement the stronghold library for our app, which is used to store sensitive data.
// Re-exports
pub use oid4vc::{oid4vc_core, oid4vc_manager, oid4vci, oid4vp, siopv2};

Expand Down
42 changes: 40 additions & 2 deletions identity-wallet/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::{
verifiable_credential_record::DisplayCredential,
};
use derivative::Derivative;
use downcast_rs::{impl_downcast, DowncastSync};
use dyn_clone::DynClone;
use oid4vc::oid4vc_core::Subject;
use oid4vc::oid4vc_manager::ProviderManager;
use oid4vc::oid4vci::Wallet;
Expand All @@ -17,6 +19,24 @@ use std::{collections::VecDeque, sync::Arc};
use strum::EnumString;
use ts_rs::TS;

/// The AppState is the main state of the application shared between the backend and the frontend.
/// We have structured the state and its operations following the redux pattern.
/// To safeguard this pattern we have introduced the FeatTrait, ActionTrait and a macro_rule for the Reducers.
/// All fields in the AppState have to implement the FeatTrait.
/// This is to ensure that the state is serializable/deserializable and cloneable among other things.
/// All actions have to implement the ActionTrait.
/// This ensures that all actions have at least one reducer, implement a debug method,
/// and are downcastable (necessary when receiving the action from the frontend)
/// The reducers are paired with the actions using our macro_rule.
/// This ensures that all reducers have the same signature and therefore follow the redux pattern and our error handling.
/// All the above goes for extensions (values) which are added to the extensions field.
/// Trait which each field of the appstate has to implement.
#[typetag::serde(tag = "feat_state_type")]
pub trait FeatTrait: Send + Sync + std::fmt::Debug + DynClone + DowncastSync {}
dyn_clone::clone_trait_object!(FeatTrait);
impl_downcast!(sync FeatTrait);

pub struct IdentityManager {
pub subject: Arc<dyn Subject>,
pub provider_manager: ProviderManager,
Expand Down Expand Up @@ -53,6 +73,9 @@ pub struct AppState {
pub user_journey: Option<serde_json::Value>,
pub connections: Vec<Connection>,
pub user_data_query: Vec<String>,
/// Extensions will bring along their own redux compliant code, in the unime folder.
#[ts(skip)]
pub extensions: std::collections::HashMap<String, Box<dyn FeatTrait>>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, TS, Clone, PartialEq, Eq, Default)]
Expand All @@ -79,13 +102,27 @@ impl Clone for AppState {
connections: self.connections.clone(),
user_data_query: self.user_data_query.clone(),
dev_mode: self.dev_mode.clone(),
extensions: self.extensions.clone(),
}
}
}

#[derive(Default)]
impl AppState {
pub fn insert_extension(mut self, key: &str, extension: Box<dyn FeatTrait>) -> Self {
self.extensions.insert(key.to_string(), extension);
self
}
}

#[derive(Default, Debug)]
pub struct AppStateContainer(pub tokio::sync::Mutex<AppState>);

impl AppStateContainer {
pub async fn insert_extension(self, key: &str, extension: Box<dyn FeatTrait>) -> Self {
self.0.lock().await.extensions.insert(key.to_string(), extension);
self
}
}
/// Format of a locale string: `ll_CC` - where ll is the language code (ISO 639) and CC is the country code (ISO 3166).
#[derive(Clone, Serialize, Debug, Deserialize, TS, PartialEq, Default, EnumString)]
#[ts(export)]
Expand Down Expand Up @@ -174,7 +211,8 @@ mod tests {
"debug_messages": [],
"user_journey": null,
"connections": [],
"user_data_query": []
"user_data_query": [],
"extensions": {}
}"#}
);
}
Expand Down
4 changes: 3 additions & 1 deletion identity-wallet/src/state/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ pub async fn save_state(app_state: &AppState) -> anyhow::Result<()> {
Ok(())
}

// Removes the state file from the app's data directory.
/// Removes the state file from the app's data directory.
pub async fn delete_state_file() -> anyhow::Result<()> {
let state_file = STATE_FILE.lock().unwrap().clone();
remove_file(state_file).await?;
debug!("state deleted from disk");
Ok(())
}

/// Removes the stronghold file from the app's data directory.
pub async fn delete_stronghold() -> anyhow::Result<()> {
let stronghold_file = crate::STRONGHOLD.lock().unwrap().clone();
remove_file(&stronghold_file).await?;
Expand Down Expand Up @@ -68,6 +69,7 @@ pub fn clear_all_assets() -> Result<(), AppError> {
Ok(())
}

/// Persists an asset from the `/assets/tmp` folder to the `/assets` folder inside the system-specific data directory.
pub fn persist_asset(file_name: &str, id: &str) -> Result<(), AppError> {
let assets_dir = ASSETS_DIR.lock().unwrap().as_path().to_owned();
let tmp_dir = assets_dir.join("tmp");
Expand Down
1 change: 1 addition & 0 deletions unime/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ tauri = { version = "=2.0.0-alpha.21", features = ["rustls-tls", "test"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.26.0", features = ["macros"] }
url = "2.4"
typetag = "0.2"

[features]
# this feature is used for production builds or when `devPath` points to the filesystem
Expand Down
21 changes: 21 additions & 0 deletions unime/src-tauri/tests/common/extensions/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use identity_wallet::{
reducer,
state::{actions::ActionTrait, reducers::Reducer},
};

use crate::common::extensions::reducers::test_feat_state;

/// Action to test the extension field.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct CustomExtensionTest {
pub test_term: Option<String>,
#[serde(default)]
pub test_bool: bool,
}

#[typetag::serde(name = "[Test] Test")]
impl ActionTrait for CustomExtensionTest {
fn reducers<'a>(&self) -> Vec<Reducer<'a>> {
vec![reducer!(test_feat_state)]
}
}
15 changes: 15 additions & 0 deletions unime/src-tauri/tests/common/extensions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use identity_wallet::state::FeatTrait;
use serde::{Deserialize, Serialize};

pub mod actions;
pub mod reducers;

// This module is soly for testing and demonstrating the extension system.
#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
pub struct CustomExtension {
pub name: String,
pub value: String,
}

#[typetag::serde(name = "custom_extension")]
impl FeatTrait for CustomExtension {}
27 changes: 27 additions & 0 deletions unime/src-tauri/tests/common/extensions/reducers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use identity_wallet::error::AppError;
use identity_wallet::state::actions::{listen, Action};
use identity_wallet::state::AppState;

use super::actions::CustomExtensionTest;
use super::CustomExtension;

pub async fn test_feat_state(state: AppState, action: Action) -> Result<AppState, AppError> {
if let Some(test_feat_state) = listen::<CustomExtensionTest>(action) {
let mut new_state = state;

new_state.extensions.insert(
"test".to_string(),
Box::new(CustomExtension {
name: "new".to_string(),
value: if test_feat_state.test_bool {
"new".to_string()
} else {
"old".to_string()
},
}),
);

return Ok(new_state);
}
Ok(state)
}
1 change: 1 addition & 0 deletions unime/src-tauri/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::path::Path;
use std::sync::Arc;

pub mod assert_state_update;
pub mod extensions;

pub const TEST_PASSWORD: &str = "sup3rSecr3t";

Expand Down
7 changes: 7 additions & 0 deletions unime/src-tauri/tests/fixtures/actions/test_extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "[Test] Test",
"payload": {
"test_term": "test",
"test_bool": true
}
}
9 changes: 9 additions & 0 deletions unime/src-tauri/tests/fixtures/states/test_extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extensions": {
"test": {
"feat_state_type": "custom_extension",
"name": "new",
"value": "new"
}
}
}
43 changes: 43 additions & 0 deletions unime/src-tauri/tests/tests/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::common::assert_state_update::{assert_state_update, setup_state_file, setup_stronghold};
use crate::common::extensions::CustomExtension;
use crate::common::json_example;
use identity_wallet::state::AppStateContainer;
use identity_wallet::state::{actions::Action, AppState};

#[tokio::test]
#[serial_test::serial]
async fn test_extension() {
setup_state_file();
setup_stronghold();

// Deserializing the AppStates and Actions from the accompanying json files.
let state = AppStateContainer::default()
.insert_extension(
"test",
Box::new(CustomExtension {
name: "test".to_string(),
value: "test".to_string(),
}),
)
.await;
let state2 = json_example::<AppState>("tests/fixtures/states/test_extension.json");
let action1 = json_example::<Action>("tests/fixtures/actions/test_extension.json");

dbg!(&state);
dbg!(&state2);
dbg!(&action1);

assert_state_update(
// Initial state.
state,
vec![
// Test action
action1,
],
vec![
// state including CustomExtension
Some(state2),
],
)
.await;
}
1 change: 1 addition & 0 deletions unime/src-tauri/tests/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod credential_offer;
mod extensions;
mod get_state;
mod load_dev_profile;
mod qr_code_scanned;
Expand Down

0 comments on commit 7b7e4ff

Please sign in to comment.