diff --git a/giterated-daemon/src/backend/git.rs b/giterated-daemon/src/backend/git.rs index 872e72e..704e56e 100644 --- a/giterated-daemon/src/backend/git.rs +++ b/giterated-daemon/src/backend/git.rs @@ -5,16 +5,16 @@ use futures_util::StreamExt; use giterated_models::instance::{Instance, RepositoryCreateRequest}; use giterated_models::repository::{ - Commit, IssueLabel, Repository, RepositoryDescription, RepositoryFileInspectRequest, - RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, - RepositoryIssuesRequest, RepositorySummary, RepositoryTreeEntry, RepositoryVisibility, - RepositoryVisibilitySetting, RepositoryObjectType, Description, Visibility, LatestCommit, DefaultBranch, + Commit, DefaultBranch, Description, IssueLabel, LatestCommit, Repository, + RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, + RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryObjectType, + RepositoryTreeEntry, RepositoryVisibility, Visibility, }; use giterated_models::settings::{AnySetting, Setting}; use giterated_models::user::{User, UserParseError}; use giterated_models::value::{AnyValue, GiteratedObjectValue}; use serde_json::Value; -use sqlx::{Either, PgPool}; +use sqlx::PgPool; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -296,24 +296,13 @@ impl RepositoryBackend for GitBackend { ) -> Result, Error> { Ok(unsafe { if name == Description::value_name() { - info!("Description"); - AnyValue::from_raw( - self.get_setting(repository, RepositoryDescription::name()) - .await? - .0, - ) + AnyValue::from_raw(self.get_setting(repository, Description::name()).await?.0) } else if name == Visibility::value_name() { - AnyValue::from_raw( - self.get_setting(repository, RepositoryVisibilitySetting::name()) - .await? - .0, - ) + AnyValue::from_raw(self.get_setting(repository, Visibility::name()).await?.0) } else if name == DefaultBranch::value_name() { - AnyValue::from_raw( - self.get_setting(repository, DefaultBranch::name()) - .await? - .0, - ) + AnyValue::from_raw(self.get_setting(repository, DefaultBranch::name()).await?.0) + } else if name == LatestCommit::value_name() { + AnyValue::from_raw(serde_json::to_value(LatestCommit(None)).unwrap()) } else { return Err(UserParseError.into()); } @@ -645,14 +634,6 @@ impl RepositoryBackend for GitBackend { Ok(tree) } - - async fn repositories_for_user( - &mut self, - requester: Option<&User>, - user: &User, - ) -> Result, Error> { - unimplemented!(); - } } impl IssuesBackend for GitBackend { diff --git a/giterated-daemon/src/backend/mod.rs b/giterated-daemon/src/backend/mod.rs index 2a59ac5..a3db4f3 100644 --- a/giterated-daemon/src/backend/mod.rs +++ b/giterated-daemon/src/backend/mod.rs @@ -37,11 +37,6 @@ pub trait RepositoryBackend { repository: &Repository, request: &RepositoryFileInspectRequest, ) -> Result, Error>; - async fn repositories_for_user( - &mut self, - requester: Option<&User>, - user: &User, - ) -> Result, Error>; async fn get_value( &mut self, user: &Repository, @@ -100,6 +95,11 @@ pub trait UserBackend: AuthBackend { setting: &Value, ) -> Result<(), Error>; async fn exists(&mut self, user: &User) -> Result; + async fn repositories_for_user( + &mut self, + requester: Option<&User>, + user: &User, + ) -> Result, Error>; } #[async_trait::async_trait] diff --git a/giterated-daemon/src/backend/settings.rs b/giterated-daemon/src/backend/settings.rs index 92216e5..0cdeedd 100644 --- a/giterated-daemon/src/backend/settings.rs +++ b/giterated-daemon/src/backend/settings.rs @@ -49,7 +49,7 @@ impl MetadataBackend for DatabaseSettings { let row = sqlx::query_as!( RepositorySettingRow, "SELECT * FROM repository_settings WHERE repository = $1 AND name = $2", - format!("{}/{}", repository.owner, repository.name), + repository.to_string(), name ) .fetch_one(&self.pg_pool) diff --git a/giterated-daemon/src/backend/user.rs b/giterated-daemon/src/backend/user.rs index cc7cc8e..98af009 100644 --- a/giterated-daemon/src/backend/user.rs +++ b/giterated-daemon/src/backend/user.rs @@ -1,10 +1,12 @@ use anyhow::Error; +use futures_util::StreamExt; use giterated_models::authenticated::UserAuthenticationToken; use giterated_models::instance::{AuthenticationTokenRequest, Instance, RegisterAccountRequest}; +use giterated_models::repository::{Repository, RepositorySummary}; use giterated_models::settings::{AnySetting, Setting}; -use giterated_models::user::{User, UserBio, UserDisplayName, UserParseError}; +use giterated_models::user::{Bio, DisplayName, User, UserParseError}; use giterated_models::value::AnyValue; use std::sync::Arc; @@ -20,10 +22,11 @@ use rsa::{ use secrecy::ExposeSecret; use serde_json::Value; -use sqlx::PgPool; +use sqlx::{Either, PgPool}; use tokio::sync::Mutex; use crate::authentication::AuthenticationTokenGranter; +use crate::backend::git::GitRepository; use super::{AuthBackend, MetadataBackend, UserBackend}; @@ -55,11 +58,9 @@ impl UserBackend for UserAuth { async fn get_value(&mut self, user: &User, name: &str) -> Result, Error> { Ok(match name { "display_name" => unsafe { - AnyValue::from_raw(self.get_setting(user, UserDisplayName::name()).await?.0) - }, - "bio" => unsafe { - AnyValue::from_raw(self.get_setting(user, UserBio::name()).await?.0) + AnyValue::from_raw(self.get_setting(user, DisplayName::name()).await?.0) }, + "bio" => unsafe { AnyValue::from_raw(self.get_setting(user, Bio::name()).await?.0) }, _ => { return Err(UserParseError.into()); } @@ -94,6 +95,37 @@ impl UserBackend for UserAuth { .await .is_ok()) } + + async fn repositories_for_user( + &mut self, + _requester: Option<&User>, + user: &User, + ) -> Result, Error> { + let mut repositories = sqlx::query_as!( + GitRepository, + r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1"#, + user.to_string() + ) + .fetch_many(&self.pg_pool); + + let mut return_repositories = vec![]; + + while let Some(Ok(Either::Right(repository_row))) = repositories.next().await { + return_repositories.push(RepositorySummary { + repository: Repository { + owner: repository_row.owner_user.clone(), + name: repository_row.name, + instance: self.this_instance.clone(), + }, + owner: repository_row.owner_user, + visibility: repository_row.visibility, + description: repository_row.description, + last_commit: None, + }) + } + + Ok(return_repositories) + } } #[async_trait::async_trait] @@ -183,7 +215,6 @@ impl AuthBackend for UserAuth { source: &Instance, request: AuthenticationTokenRequest, ) -> Result { - info!("fetching!"); let user = sqlx::query_as!( UserRow, r#"SELECT * FROM users WHERE username = $1"#, @@ -198,7 +229,6 @@ impl AuthBackend for UserAuth { .verify_password(request.password.expose_secret().0.as_bytes(), &hash) .is_err() { - info!("invalid password"); return Err(Error::from(AuthenticationError::InvalidPassword)); } diff --git a/giterated-daemon/src/cache_backend.rs b/giterated-daemon/src/cache_backend.rs index cbdfb8d..31bd28b 100644 --- a/giterated-daemon/src/cache_backend.rs +++ b/giterated-daemon/src/cache_backend.rs @@ -14,8 +14,8 @@ impl ObjectBackend for CacheBackend { async fn object_operation + Debug>( &self, _object: O, - operation: &str, - payload: D, + _operation: &str, + _payload: D, ) -> Result> { // We don't handle operations with this backend Err(OperationError::Unhandled) diff --git a/giterated-daemon/src/connection/wrapper.rs b/giterated-daemon/src/connection/wrapper.rs index ce24414..9998b93 100644 --- a/giterated-daemon/src/connection/wrapper.rs +++ b/giterated-daemon/src/connection/wrapper.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::Error; use futures_util::{SinkExt, StreamExt}; -use giterated_models::instance::Instance; +use giterated_models::{error::OperationError, instance::Instance}; use giterated_models::object_backend::ObjectBackend; @@ -87,12 +87,23 @@ pub async fn connection_wrapper( .object_operation(message.object, &message.operation, message.payload) .await; + // Map result to Vec on both + let result = match result { + Ok(result) => Ok(serde_json::to_vec(&result).unwrap()), + Err(err) => Err(match err { + OperationError::Operation(err) => { + OperationError::Operation(serde_json::to_vec(&err).unwrap()) + } + OperationError::Internal(err) => OperationError::Internal(err), + OperationError::Unhandled => OperationError::Unhandled, + }), + }; + let mut socket = connection_state.socket.lock().await; let _ = socket .send(Message::Binary(bincode::serialize(&result).unwrap())) .await; - info!("Done!"); drop(socket); } _ => { diff --git a/giterated-daemon/src/database_backend/handler.rs b/giterated-daemon/src/database_backend/handler.rs index 6fe5a89..73f7f4d 100644 --- a/giterated-daemon/src/database_backend/handler.rs +++ b/giterated-daemon/src/database_backend/handler.rs @@ -1,14 +1,18 @@ -use std::{collections::HashMap, pin::Pin, sync::Arc}; +use std::{collections::HashMap, error::Error, pin::Pin, sync::Arc}; use futures_util::{future::BoxFuture, Future, FutureExt}; use giterated_models::{ - error::{GetValueError, OperationError, UserError, RepositoryError}, + error::{GetValueError, OperationError, RepositoryError, UserError}, object::{AnyObject, GiteratedObject}, + object_backend::ObjectBackend, operation::{AnyOperation, GiteratedOperation}, - repository::{Repository, RepositorySummary, RepositoryInfoRequest, RepositoryView, Description, Visibility, DefaultBranch, LatestCommit, RepositoryFileInspectRequest}, + repository::{ + DefaultBranch, Description, LatestCommit, Repository, RepositoryFileInspectRequest, + RepositoryInfoRequest, RepositorySummary, RepositoryView, Visibility, + }, settings::{AnySetting, GetSetting, GetSettingError, SetSetting, SetSettingError}, user::{User, UserRepositoriesRequest}, - value::{AnyValue, GetValue}, object_backend::ObjectBackend, + value::{AnyValue, GetValue}, }; use super::DatabaseBackend; @@ -122,14 +126,14 @@ impl OperationWrapper { pub fn user_get_repositories( object: &User, - operation: UserRepositoriesRequest, + _operation: UserRepositoriesRequest, state: DatabaseBackend, ) -> BoxFuture<'static, Result, OperationError>> { let object = object.clone(); async move { - let mut repositories_backend = state.repository_backend.lock().await; - let repositories = repositories_backend + let mut user_backend = state.user_backend.lock().await; + let repositories = user_backend .repositories_for_user(None, &object) .await .map_err(|e| OperationError::Internal(e.to_string()))?; @@ -199,26 +203,50 @@ pub fn user_set_setting( pub fn repository_info( object: &Repository, operation: RepositoryInfoRequest, - state: DatabaseBackend + state: DatabaseBackend, ) -> BoxFuture<'static, Result>> { let object = object.clone(); async move { - let mut object = state.get_object::(&object.to_string()).await.unwrap(); + let mut object = state + .get_object::(&object.to_string()) + .await + .unwrap(); let mut repository_backend = state.repository_backend.lock().await; - let tree = repository_backend.repository_file_inspect(None, object.object(), &RepositoryFileInspectRequest { - extra_metadata: operation.extra_metadata, - path: operation.path, - rev: operation.rev.clone(), - }).await.map_err(|err| OperationError::Internal(format!("{:?}", err)))?; - + let tree = repository_backend + .repository_file_inspect( + None, + object.object(), + &RepositoryFileInspectRequest { + extra_metadata: operation.extra_metadata, + path: operation.path, + rev: operation.rev.clone(), + }, + ) + .await + .map_err(|err| OperationError::Internal(format!("{:?}", err)))?; + drop(repository_backend); + + let _visibility = object.get::().await.map_err(|e| { + OperationError::Internal(format!("{:?}: {}", e.source(), e.to_string())) + })?; + let _default_branch = object.get::().await.map_err(|e| { + OperationError::Internal(format!("{:?}: {}", e.source(), e.to_string())) + })?; + let info = RepositoryView { name: object.object().name.clone(), owner: object.object().owner.clone(), description: object.get::().await.ok(), - visibility: object.get::().await.map_err(|e| OperationError::Internal(e.to_string()))?, - default_branch: object.get::().await.map_err(|e| OperationError::Internal(e.to_string()))?, + visibility: object + .get::() + .await + .map_err(|e| OperationError::Internal(e.to_string()))?, + default_branch: object + .get::() + .await + .map_err(|e| OperationError::Internal(e.to_string()))?, // TODO: Can't be a simple get function, this needs to be returned alongside the tree as this differs depending on the rev and path. latest_commit: object.get::().await.ok(), tree_rev: operation.rev, @@ -242,7 +270,9 @@ pub fn repository_get_value( let value = repository_backend .get_value(&object, &operation.value_name) .await - .map_err(|e| OperationError::Internal(e.to_string()))?; + .map_err(|e| { + OperationError::Internal(format!("error getting value: {}", e.to_string())) + })?; Ok(value) } diff --git a/giterated-daemon/src/database_backend/mod.rs b/giterated-daemon/src/database_backend/mod.rs index ebf4f2b..b490bce 100644 --- a/giterated-daemon/src/database_backend/mod.rs +++ b/giterated-daemon/src/database_backend/mod.rs @@ -1,15 +1,14 @@ pub mod handler; -use std::any::type_name; use std::{str::FromStr, sync::Arc}; use giterated_models::error::OperationError; use giterated_models::instance::Instance; use giterated_models::object::{ - AnyObject, GiteratedObject, Object, ObjectRequest, ObjectRequestError, ObjectResponse, + AnyObject, GiteratedObject, Object, ObjectRequest, ObjectRequestError, }; use giterated_models::object_backend::ObjectBackend; -use giterated_models::operation::{AnyOperation, GiteratedOperation}; +use giterated_models::operation::GiteratedOperation; use giterated_models::repository::Repository; use giterated_models::user::User; use std::fmt::Debug; @@ -18,8 +17,8 @@ use tokio::sync::Mutex; use crate::backend::{RepositoryBackend, UserBackend}; use self::handler::{ - repository_get_setting, repository_get_value, repository_set_setting, user_get_repositories, - user_get_setting, user_get_value, user_set_setting, OperationHandlers, repository_info, + repository_get_setting, repository_get_value, repository_info, repository_set_setting, + user_get_repositories, user_get_setting, user_get_value, user_set_setting, OperationHandlers, }; #[derive(Clone, Debug)] @@ -30,8 +29,8 @@ impl ObjectBackend for Foobackend { async fn object_operation + Debug>( &self, _object: O, - operation: &str, - payload: D, + _operation: &str, + _payload: D, ) -> Result> { // We don't handle operations with this backend Err(OperationError::Unhandled) @@ -102,10 +101,8 @@ impl ObjectBackend for DatabaseBackend { ) .await { - Ok(result) => Ok( - serde_json::from_slice(&serde_json::to_vec(&result).unwrap()) - .map_err(|e| OperationError::Internal(e.to_string()))?, - ), + Ok(result) => Ok(serde_json::from_slice(&result) + .map_err(|e| OperationError::Internal(e.to_string()))?), Err(err) => match err { OperationError::Internal(internal) => Err(OperationError::Internal(internal)), OperationError::Unhandled => Err(OperationError::Unhandled), @@ -133,10 +130,8 @@ impl ObjectBackend for DatabaseBackend { ) .await { - Ok(result) => Ok( - serde_json::from_slice(&serde_json::to_vec(&result).unwrap()) - .map_err(|e| OperationError::Internal(e.to_string()))?, - ), + Ok(result) => Ok(serde_json::from_slice(&result) + .map_err(|e| OperationError::Internal(e.to_string()))?), Err(err) => match err { OperationError::Internal(internal) => Err(OperationError::Internal(internal)), OperationError::Unhandled => Err(OperationError::Unhandled), @@ -167,10 +162,8 @@ impl ObjectBackend for DatabaseBackend { }); match result { - Ok(result) => Ok( - serde_json::from_slice(&serde_json::to_vec(&result).unwrap()) - .map_err(|e| OperationError::Internal(e.to_string()))?, - ), + Ok(result) => Ok(serde_json::from_slice(&result) + .map_err(|e| OperationError::Internal(e.to_string()))?), Err(err) => match err { OperationError::Internal(internal) => { Err(OperationError::Internal(internal)) @@ -265,7 +258,7 @@ mod test { RepositoryTreeEntry, }; use giterated_models::settings::AnySetting; - use giterated_models::user::{DisplayName, User, UserDisplayName}; + use giterated_models::user::{DisplayName, User}; use giterated_models::value::{AnyValue, GiteratedObjectValue}; use serde_json::Value; use tokio::sync::Mutex; @@ -302,6 +295,13 @@ mod test { async fn exists(&mut self, user: &User) -> Result { Ok(user == &User::from_str("test_user:test.giterated.dev").unwrap()) } + async fn repositories_for_user( + &mut self, + requester: Option<&User>, + user: &User, + ) -> Result, Error> { + todo!() + } } #[async_trait::async_trait] @@ -341,14 +341,6 @@ mod test { ) -> Result, Error> { todo!() } - async fn repositories_for_user( - &mut self, - _requester: Option<&User>, - _user: &User, - ) -> Result, Error> { - todo!() - } - async fn get_value( &mut self, _repository: &Repository, @@ -419,7 +411,7 @@ mod test { .await .expect("object should have been returned"); - user.get_setting::() + user.get_setting::() .await .expect("object value should have been returned"); } @@ -433,7 +425,7 @@ mod test { .await .expect("object should have been returned"); - user.set_setting::(UserDisplayName(String::from("test"))) + user.set_setting::(DisplayName(String::from("test"))) .await .expect("object value should have been returned"); } diff --git a/giterated-models/src/error.rs b/giterated-models/src/error.rs index e4bbf09..9015c5d 100644 --- a/giterated-models/src/error.rs +++ b/giterated-models/src/error.rs @@ -17,7 +17,7 @@ pub enum UserError {} #[derive(Debug, thiserror::Error, Serialize, Deserialize)] pub enum GetValueError { #[error("invalid object")] - InvalidObject + InvalidObject, } #[derive(Serialize, Deserialize, Debug, thiserror::Error)] diff --git a/giterated-models/src/object.rs b/giterated-models/src/object.rs index 1b648cb..6cac03d 100644 --- a/giterated-models/src/object.rs +++ b/giterated-models/src/object.rs @@ -56,11 +56,14 @@ impl<'b, O: GiteratedObject + Clone + Debug, B: ObjectBackend> Object<'b, O, B> pub async fn get + Send + Debug>( &mut self, ) -> Result> { - self.request(GetValue { - value_name: V::value_name().to_string(), - _marker: PhantomData, - }) - .await + let result = self + .request(GetValue { + value_name: V::value_name().to_string(), + _marker: PhantomData, + }) + .await; + + result } pub async fn get_setting( diff --git a/giterated-models/src/operation.rs b/giterated-models/src/operation.rs index 53d949d..b816649 100644 --- a/giterated-models/src/operation.rs +++ b/giterated-models/src/operation.rs @@ -20,7 +20,7 @@ pub trait GiteratedOperation: Send + Serialize + Deserialize pub struct AnyOperation(pub Value); impl GiteratedOperation for AnyOperation { - type Success = Vec; + type Success = Value; - type Failure = Vec; + type Failure = Value; } diff --git a/giterated-models/src/repository/operations.rs b/giterated-models/src/repository/operations.rs index 611fbd4..1900d34 100644 --- a/giterated-models/src/repository/operations.rs +++ b/giterated-models/src/repository/operations.rs @@ -7,7 +7,7 @@ use crate::{ operation::GiteratedOperation, }; -use super::{IssueLabel, Repository, RepositoryIssue, RepositoryTreeEntry, RepositoryView, Commit}; +use super::{IssueLabel, Repository, RepositoryIssue, RepositoryTreeEntry, RepositoryView}; /// A request to get a repository's information. /// @@ -97,7 +97,12 @@ impl GiteratedOperation for RepositoryFileInspectRequest { } impl Object<'_, Repository, B> { - pub async fn info(&mut self, extra_metadata: bool, rev: Option, path: Option) -> Result> { + pub async fn info( + &mut self, + extra_metadata: bool, + rev: Option, + path: Option, + ) -> Result> { self.request::(RepositoryInfoRequest { extra_metadata, rev, diff --git a/giterated-models/src/repository/settings.rs b/giterated-models/src/repository/settings.rs index fd73066..f34b877 100644 --- a/giterated-models/src/repository/settings.rs +++ b/giterated-models/src/repository/settings.rs @@ -1,29 +1,9 @@ -use serde::{Deserialize, Serialize}; - use crate::settings::Setting; use super::DefaultBranch; -#[derive(Debug, Serialize, Deserialize)] -pub struct RepositoryDescription(pub String); - -impl Setting for RepositoryDescription { - fn name() -> &'static str { - "Repository Description" - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct RepositoryVisibilitySetting(pub String); - -impl Setting for RepositoryVisibilitySetting { - fn name() -> &'static str { - "Repository Visibility" - } -} - impl Setting for DefaultBranch { fn name() -> &'static str { - "Default Branch" + "default_branch" } -} \ No newline at end of file +} diff --git a/giterated-models/src/repository/values.rs b/giterated-models/src/repository/values.rs index 5ebfec0..e55dd49 100644 --- a/giterated-models/src/repository/values.rs +++ b/giterated-models/src/repository/values.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{settings::Setting, value::GiteratedObjectValue}; -use super::{Repository, RepositoryVisibility, Commit}; +use super::{Commit, Repository, RepositoryVisibility}; // pub struct RepositorySetting(pub V); @@ -42,6 +42,8 @@ impl Setting for Description { } #[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[repr(transparent)] +#[serde(transparent)] pub struct Visibility(pub RepositoryVisibility); impl GiteratedObjectValue for Visibility { @@ -52,9 +54,23 @@ impl GiteratedObjectValue for Visibility { } } +impl Setting for Visibility { + fn name() -> &'static str { + "visibility" + } +} + #[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +#[repr(transparent)] pub struct DefaultBranch(pub String); +impl Display for DefaultBranch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + impl GiteratedObjectValue for DefaultBranch { type Object = Repository; @@ -64,7 +80,7 @@ impl GiteratedObjectValue for DefaultBranch { } #[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct LatestCommit(pub Commit); +pub struct LatestCommit(pub Option); impl GiteratedObjectValue for LatestCommit { type Object = Repository; diff --git a/giterated-models/src/user/settings.rs b/giterated-models/src/user/settings.rs index bb38c06..8b34e2f 100644 --- a/giterated-models/src/user/settings.rs +++ b/giterated-models/src/user/settings.rs @@ -1,30 +1,15 @@ -use serde::{Deserialize, Serialize}; - use crate::settings::Setting; -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct UserBio(pub String); +use super::{Bio, DisplayName}; -impl Setting for UserBio { +impl Setting for Bio { fn name() -> &'static str { - "Bio" + "bio" } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct UserDisplayName(pub String); - -impl Setting for UserDisplayName { - fn name() -> &'static str { - "Display Name" - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct UserDisplayImage(pub String); - -impl Setting for UserDisplayImage { +impl Setting for DisplayName { fn name() -> &'static str { - "Profile Image" + "display_name" } }