// Copyright 2022 Jeremy Wall // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; use base64::{self, Engine}; use chrono::NaiveDate; use gloo_net; // TODO(jwall): Remove this when we have gone a few migrations past. //use serde_json::{from_str, to_string}; use sycamore::prelude::*; use tracing::{debug, error, instrument}; use anyhow::Result; use client_api::*; use recipes::{IngredientKey, RecipeEntry}; use serde_wasm_bindgen::{from_value, to_value}; use wasm_bindgen::JsValue; // TODO(jwall): Remove this when we have gone a few migrations past. //use web_sys::Storage; use crate::{ app_state::{parse_recipes, AppState}, js_lib::{self, DBFactory}, }; #[allow(dead_code)] #[derive(Debug)] pub struct Error(String); impl From for Error { fn from(item: std::io::Error) -> Self { Error(format!("{:?}", item)) } } impl From for String { fn from(item: Error) -> Self { format!("{:?}", item) } } impl From for Error { fn from(item: JsValue) -> Self { Error(format!("{:?}", item)) } } impl From for Error { fn from(item: String) -> Self { Error(item) } } impl From<&'static str> for Error { fn from(item: &'static str) -> Self { Error(item.to_owned()) } } impl From for Error { fn from(item: std::string::FromUtf8Error) -> Self { Error(format!("{:?}", item)) } } impl From for Error { fn from(item: gloo_net::Error) -> Self { Error(format!("{:?}", item)) } } fn recipe_key(id: S) -> String { format!("recipe:{}", id) } fn token68(user: String, pass: String) -> String { base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass)) } #[derive(Clone, Debug)] pub struct LocalStore { // FIXME(zaphar): Migration from local storage to indexed db //old_store: Storage, store: DBFactory<'static>, } const APP_STATE_KEY: &'static str = "app-state"; impl LocalStore { pub fn new() -> Self { Self { store: DBFactory::new("app-state", Some(1)), //old_store: js_lib::get_storage(), } } pub async fn store_app_state(&self, state: &AppState) { self.migrate_local_store().await; let state = match to_value(state) { Ok(state) => state, Err(err) => { error!(?err, ?state, "Error deserializing app_state"); return; } }; let key = to_value(APP_STATE_KEY).expect("Failed to serialize key"); self.store .rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .put_kv( &key, &state, ) .await .expect("Failed to write to object store"); Ok(()) }) .await .expect("Failed to store app-state"); // FIXME(zaphar): Migration from local storage to indexed db //self.store // .set("app_state", &state) // .expect("Failed to set our app state"); } pub async fn fetch_app_state(&self) -> Option { debug!("Loading state from local store"); let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes"); self.store .ro_transaction(|trx| async move { let key = to_value(APP_STATE_KEY).expect("Failed to serialize key"); let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); let mut app_state: AppState = match object_store.get(&key).await.expect("Failed to read from") { Some(s) => from_value(s).expect("Failed to deserialize app state"), None => return Ok(None), }; if let Some(recipes) = recipes { debug!("Populating recipes"); for (id, recipe) in recipes { debug!(id, "Adding recipe from local storage"); app_state.recipes.insert(id, recipe); } } Ok(Some(app_state)) }) .await .expect("Failed to fetch app-state") // FIXME(zaphar): Migration from local storage to indexed db //self.store.get("app_state").map_or(None, |val| { // val.map(|s| { // debug!("Found an app_state object"); // let mut app_state: AppState = // from_str(&s).expect("Failed to deserialize app state"); // let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes"); // if let Some(recipes) = recipes { // debug!("Populating recipes"); // for (id, recipe) in recipes { // debug!(id, "Adding recipe from local storage"); // app_state.recipes.insert(id, recipe); // } // } // app_state // }) //}) } /// Gets user data from local storage. pub async fn get_user_data(&self) -> Option { self.store .ro_transaction(|trx| async move { let key = to_value("user_data").expect("Failed to serialize key"); let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); let user_data: UserData = match object_store .get(&key) .await .expect("Failed to read from object store") { Some(s) => from_value(s).expect("Failed to deserialize app state"), None => return Ok(None), }; Ok(Some(user_data)) }) .await .expect("Failed to fetch user_data") // FIXME(zaphar): Migration from local storage to indexed db //self.store // .get("user_data") // .map_or(None, |val| val.map(|val| from_str(&val).unwrap_or(None))) // .flatten() } // Set's user data to local storage. pub async fn set_user_data(&self, data: Option<&UserData>) { let key = to_value("user_data").expect("Failed to serialize key"); if let Some(data) = data { let data = data.clone(); self.store .rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .put_kv( &key, &to_value(&data).expect("failed to serialize UserData"), ) .await .expect("Failed to store user_data"); Ok(()) }) .await .expect("Failed to set user_data"); // FIXME(zaphar): Migration from local storage to indexed db //self.store // .set( // "user_data", // &to_string(data).expect("Failed to desrialize user_data"), // ) // .expect("Failed to set user_data"); } else { self.store .rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .delete(&key) .await .expect("Failed to delete user_data"); Ok(()) }) .await .expect("Failed to delete user_data"); // FIXME(zaphar): Migration from local storage to indexed db //self.store // .delete("user_data") // .expect("Failed to delete user_data"); } } async fn get_storage_keys(&self) -> Vec { self.store .ro_transaction(|trx| async move { let mut keys = Vec::new(); let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); let key_vec = object_store .get_all_keys(None) .await .expect("Failed to get keys from object_store"); for k in key_vec { if let Ok(v) = from_value(k) { keys.push(v); } } Ok(keys) }) .await .expect("Failed to get storage keys") } async fn migrate_local_store(&self) { // FIXME(zaphar): Migration from local storage to indexed db for k in self.get_storage_keys().await.into_iter().filter(|k| { k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples" }) { // Deleting old local store key let key = to_value(&k).expect("Failed to serialize key"); self.store .rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .delete(&key) .await .expect("Failed to delete user_data"); Ok(()) }) .await .expect("Failed to delete user_data"); //debug!("Deleting old local store key {}", k); //self.store.delete(&k).expect("Failed to delete storage key"); } } async fn get_recipe_keys(&self) -> impl Iterator { self.get_storage_keys() .await .into_iter() .filter(|k| k.starts_with("recipe:")) } /// Gets all the recipes from local storage. pub async fn get_recipes(&self) -> Option> { let mut recipe_list = Vec::new(); for recipe_key in self.get_recipe_keys().await { let key = to_value(&recipe_key).expect("Failed to serialize key"); let entry = self .store .ro_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); let entry: Option = match object_store .get(&key) .await .expect("Failed to get recipe from key") { Some(v) => from_value(v).expect("Failed to deserialize entry"), None => None, }; Ok(entry) }) .await .expect("Failed to get recipes"); if let Some(entry) = entry { recipe_list.push(entry); } } if recipe_list.is_empty() { return None; } Some(recipe_list) } pub async fn get_recipe_entry(&self, id: &str) -> Option { let key = to_value(&recipe_key(id)).expect("Failed to serialize key"); self.store .ro_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); let entry: Option = match object_store .get(&key) .await .expect("Failed to get recipe from key") { Some(v) => from_value(v).expect("Failed to deserialize entry"), None => None, }; Ok(entry) }) .await .expect("Failed to get recipes") // FIXME(zaphar): Migration from local storage to indexed db //self.store // .get(&key) // .expect(&format!("Failed to get recipe {}", key)) // .map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key))) } /// Sets the set of recipes to the entries passed in. Deletes any recipes not /// in the list. pub async fn set_all_recipes(&self, entries: &Vec) { // FIXME(zaphar): Migration from local storage to indexed db for recipe_key in self.get_recipe_keys().await { let key = to_value(&recipe_key).expect("Failed to serialize key"); self.store .rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .delete(&key) .await .expect("Failed to delete user_data"); Ok(()) }) .await .expect("Failed to delete user_data"); //self.store // .delete(&recipe_key) // .expect(&format!("Failed to get recipe {}", recipe_key)); } for entry in entries { let entry = entry.clone(); let key = to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key"); self.store.rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .put_kv( &key, &to_value(&entry).expect("Failed to serialize recipe entry"), ) .await .expect("Failed to store recipe_entry"); Ok(()) }).await.expect("Failed to store recipe entry"); //self.set_recipe_entry(entry).await; } } /// Set recipe entry in local storage. pub async fn set_recipe_entry(&self, entry: &RecipeEntry) { let entry = entry.clone(); let key = to_value(&recipe_key(entry.recipe_id())).expect("Failed to serialize recipe key"); self.store.rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .put_kv( &key, &to_value(&entry).expect("Failed to serialize recipe entry"), ) .await .expect("Failed to store recipe_entry"); Ok(()) }).await.expect("Failed to store recipe entry"); // FIXME(zaphar): Migration from local storage to indexed db //self.store // .set( // &recipe_key(entry.recipe_id()), // &to_string(&entry).expect(&format!("Failed to get recipe {}", entry.recipe_id())), // ) // .expect(&format!("Failed to store recipe {}", entry.recipe_id())) } /// Delete recipe entry from local storage. pub async fn delete_recipe_entry(&self, recipe_id: &str) { let key = to_value(recipe_id).expect("Failed to serialize key"); self.store .rw_transaction(|trx| async move { let object_store = trx .object_store(js_lib::STORE_NAME) .expect("Failed to get object store"); object_store .delete(&key) .await .expect("Failed to delete user_data"); Ok(()) }) .await .expect("Failed to delete user_data"); // FIXME(zaphar): Migration from local storage to indexed db //self.store // .delete(&recipe_key(recipe_id)) // .expect(&format!("Failed to delete recipe {}", recipe_id)) } } #[derive(Clone, Debug)] pub struct HttpStore { root: String, local_store: LocalStore, } impl HttpStore { pub fn new(root: String) -> Self { Self { root, local_store: LocalStore::new(), } } pub fn v2_path(&self) -> String { let mut path = self.root.clone(); path.push_str("/v2"); path } pub fn provide_context>(cx: Scope, root: S) { provide_context(cx, std::rc::Rc::new(Self::new(root.into()))); } pub fn get_from_context(cx: Scope) -> std::rc::Rc { use_context::>(cx).clone() } // NOTE(jwall): We do **not** want to record the password in our logs. #[instrument(skip_all, fields(?self, user))] pub async fn authenticate(&self, user: String, pass: String) -> Option { debug!("attempting login request against api."); let mut path = self.v2_path(); path.push_str("/auth"); let request = gloo_net::http::Request::get(&path) .header( "authorization", format!("Basic {}", token68(user, pass)).as_str(), ) .mode(web_sys::RequestMode::SameOrigin) .credentials(web_sys::RequestCredentials::SameOrigin) .build() .expect("Failed to build request"); debug!(?request, "Sending auth request"); let result = request.send().await; if let Ok(resp) = &result { if resp.status() == 200 { let user_data = resp .json::() .await .expect("Unparseable authentication response") .as_success(); return user_data; } error!(status = resp.status(), "Login was unsuccessful") } else { error!(err=?result.unwrap_err(), "Failed to send auth request"); } return None; } #[instrument] pub async fn fetch_user_data(&self) -> Option { debug!("Retrieving User Account data"); let mut path = self.v2_path(); path.push_str("/account"); let result = gloo_net::http::Request::get(&path).send().await; if let Ok(resp) = &result { if resp.status() == 200 { let user_data = resp .json::() .await .expect("Unparseable authentication response") .as_success(); return user_data; } error!(status = resp.status(), "Login was unsuccessful") } else { error!(err=?result.unwrap_err(), "Failed to send auth request"); } return None; } //#[instrument] pub async fn fetch_categories(&self) -> Result>, Error> { let mut path = self.v2_path(); path.push_str("/category_map"); let resp = match gloo_net::http::Request::get(&path).send().await { Ok(resp) => resp, Err(gloo_net::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); return Ok(None); } Err(err) => { return Err(err)?; } }; if resp.status() == 404 { debug!("Categories returned 404"); Ok(None) } else if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); let resp = resp .json::() .await? .as_success() .unwrap(); Ok(Some(resp)) } } #[instrument] pub async fn fetch_recipes(&self) -> Result>, Error> { let mut path = self.v2_path(); path.push_str("/recipes"); let resp = match gloo_net::http::Request::get(&path).send().await { Ok(resp) => resp, Err(gloo_net::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); return Ok(self.local_store.get_recipes().await); } Err(err) => { return Err(err)?; } }; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); let entries = resp .json::() .await .map_err(|e| format!("{}", e))? .as_success(); Ok(entries) } } pub async fn fetch_recipe_text + std::fmt::Display>( &self, id: S, ) -> Result, Error> { let mut path = self.v2_path(); path.push_str("/recipe/"); path.push_str(id.as_ref()); let resp = match gloo_net::http::Request::get(&path).send().await { Ok(resp) => resp, Err(gloo_net::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); return Ok(self.local_store.get_recipe_entry(id.as_ref()).await); } Err(err) => { return Err(err)?; } }; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else if resp.status() == 404 { debug!("Recipe doesn't exist"); Ok(None) } else { debug!("We got a valid response back!"); let entry = resp .json::>>() .await .map_err(|e| format!("{}", e))? .as_success() .unwrap(); if let Some(ref entry) = entry { self.local_store.set_recipe_entry(entry).await; } Ok(entry) } } #[instrument] pub async fn delete_recipe(&self, recipe: S) -> Result<(), Error> where S: AsRef + std::fmt::Debug, { let mut path = self.v2_path(); path.push_str("/recipe"); path.push_str(&format!("/{}", recipe.as_ref())); let resp = gloo_net::http::Request::delete(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } #[instrument(skip(recipes), fields(count=recipes.len()))] pub async fn store_recipes(&self, recipes: Vec) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/recipes"); for r in recipes.iter() { if r.recipe_id().is_empty() { return Err("Recipe Ids can not be empty".into()); } } let resp = gloo_net::http::Request::post(&path) .json(&recipes) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } #[instrument(skip(categories))] pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/category_map"); let resp = gloo_net::http::Request::post(&path) .json(&categories) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } #[instrument(skip_all)] pub async fn store_app_state(&self, state: &AppState) -> Result<(), Error> { let mut plan = Vec::new(); for (key, count) in state.recipe_counts.iter() { plan.push((key.clone(), *count as i32)); } if let Some(cached_plan_date) = &state.selected_plan_date { debug!(?plan, "Saving plan data"); self.store_plan_for_date(plan, cached_plan_date).await?; debug!("Saving inventory data"); self.store_inventory_data_for_date( state.filtered_ingredients.clone(), state.modified_amts.clone(), state .extras .iter() .cloned() .collect::>(), cached_plan_date, ) .await } else { debug!("Saving plan data"); self.store_plan(plan).await?; debug!("Saving inventory data"); self.store_inventory_data( state.filtered_ingredients.clone(), state.modified_amts.clone(), state .extras .iter() .cloned() .collect::>(), ) .await } } pub async fn store_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/plan"); let resp = gloo_net::http::Request::post(&path) .json(&plan) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } pub async fn store_plan_for_date( &self, plan: Vec<(String, i32)>, date: &NaiveDate, ) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/plan"); path.push_str("/at"); path.push_str(&format!("/{}", date)); let resp = gloo_net::http::Request::post(&path) .json(&plan) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } pub async fn fetch_plan_dates(&self) -> Result>, Error> { let mut path = self.v2_path(); path.push_str("/plan"); path.push_str("/all"); let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back"); let plan = resp .json::>>() .await .map_err(|e| format!("{}", e))? .as_success(); Ok(plan) } } pub async fn delete_plan_for_date(&self, date: &NaiveDate) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/plan"); path.push_str("/at"); path.push_str(&format!("/{}", date)); let resp = gloo_net::http::Request::delete(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { Ok(()) } } pub async fn fetch_plan_for_date( &self, date: &NaiveDate, ) -> Result>, Error> { let mut path = self.v2_path(); path.push_str("/plan"); path.push_str("/at"); path.push_str(&format!("/{}", date)); let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back"); let plan = resp .json::() .await .map_err(|e| format!("{}", e))? .as_success(); Ok(plan) } } //pub async fn fetch_plan(&self) -> Result>, Error> { // let mut path = self.v2_path(); // path.push_str("/plan"); // let resp = gloo_net::http::Request::get(&path).send().await?; // if resp.status() != 200 { // Err(format!("Status: {}", resp.status()).into()) // } else { // debug!("We got a valid response back"); // let plan = resp // .json::() // .await // .map_err(|e| format!("{}", e))? // .as_success(); // Ok(plan) // } //} pub async fn fetch_inventory_for_date( &self, date: &NaiveDate, ) -> Result< ( BTreeSet, BTreeMap, Vec<(String, String)>, ), Error, > { let mut path = self.v2_path(); path.push_str("/inventory"); path.push_str("/at"); path.push_str(&format!("/{}", date)); let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back"); let InventoryData { filtered_ingredients, modified_amts, extra_items, } = resp .json::() .await .map_err(|e| format!("{}", e))? .as_success() .unwrap(); Ok(( filtered_ingredients.into_iter().collect(), modified_amts.into_iter().collect(), extra_items, )) } } pub async fn fetch_inventory_data( &self, ) -> Result< ( BTreeSet, BTreeMap, Vec<(String, String)>, ), Error, > { let mut path = self.v2_path(); path.push_str("/inventory"); let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back"); let InventoryData { filtered_ingredients, modified_amts, extra_items, } = resp .json::() .await .map_err(|e| format!("{}", e))? .as_success() .unwrap(); Ok(( filtered_ingredients.into_iter().collect(), modified_amts.into_iter().collect(), extra_items, )) } } #[instrument] pub async fn store_inventory_data_for_date( &self, filtered_ingredients: BTreeSet, modified_amts: BTreeMap, extra_items: Vec<(String, String)>, date: &NaiveDate, ) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/inventory"); path.push_str("/at"); path.push_str(&format!("/{}", date)); let filtered_ingredients: Vec = filtered_ingredients.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); debug!("Storing inventory data via API"); let resp = gloo_net::http::Request::post(&path) .json(&(filtered_ingredients, modified_amts, extra_items)) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { debug!("Invalid response back"); Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } #[instrument] pub async fn store_inventory_data( &self, filtered_ingredients: BTreeSet, modified_amts: BTreeMap, extra_items: Vec<(String, String)>, ) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/inventory"); let filtered_ingredients: Vec = filtered_ingredients.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); debug!("Storing inventory data via API"); let resp = gloo_net::http::Request::post(&path) .json(&(filtered_ingredients, modified_amts, extra_items)) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { debug!("Invalid response back"); Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } pub async fn fetch_staples(&self) -> Result, Error> { let mut path = self.v2_path(); path.push_str("/staples"); let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { debug!("Invalid response back"); Err(format!("Status: {}", resp.status()).into()) } else { Ok(resp .json::>>() .await .expect("Failed to parse staples json") .as_success() .unwrap()) } } pub async fn store_staples + serde::Serialize>( &self, content: S, ) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/staples"); let resp = gloo_net::http::Request::post(&path) .json(&content) .expect("Failed to set body") .send() .await?; if resp.status() != 200 { debug!("Invalid response back"); Err(format!("Status: {}", resp.status()).into()) } else { debug!("We got a valid response back!"); Ok(()) } } }