Split State in separate views for Api and CommHandle

- State changes can now be handled differently, depending on whether they were caused locallly (API) or by a remote peer (Comm)
- State functions have more readable names (`write...` and `update...` have similar meanings, but using different names helps readability in the respective (API/Comm) context)
This commit is contained in:
Philip (a-0) 2023-12-08 22:31:47 +01:00
parent 32bbe8a8ce
commit 98393b9bf6
16 changed files with 326 additions and 138 deletions

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use axum::Router; use axum::Router;
use tokio::{net::TcpListener, task::JoinHandle}; use tokio::{net::TcpListener, task::JoinHandle};
use crate::{state::State, config::ApiConfig}; use crate::{state::{State, ApiState}, config::ApiConfig};
mod v0; mod v0;
@ -40,11 +40,11 @@ impl From<ApiConfig> for ApiBuilder {
} }
impl ApiBuilder { impl ApiBuilder {
pub async fn build(&self, state: Arc<State>) -> Api { pub async fn build(&self, state: ApiState) -> Api {
let mut app: Router = Router::new(); let mut app: Router = Router::new();
match &self.version { match &self.version {
Some(v) if v == "v0" => app = app.nest(&format!("/{}", v), v0::get_router(state.clone())), Some(v) if v == "v0" => app = app.nest(&format!("/{}", v), v0::get_router(state)),
_ => app = app.nest("/v0", v0::get_router(state.clone())), _ => app = app.nest("/v0", v0::get_router(state)),
} }
let ip = match &self.bind_ip { let ip = match &self.bind_ip {

View file

@ -1,44 +1,49 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{Router, routing::{put, get}, extract::{Path, Json}, Extension, response::{IntoResponse, Response}, http::StatusCode}; use axum::{Router, routing::{put, get}, extract::{Path, Json}, Extension, response::{IntoResponse, Response}, http::StatusCode};
use tracing::{warn, debug};
use crate::state::{State, types::{ElementId, ElementContent}}; use crate::state::{types::{ElementId, ElementContent}, ApiState};
pub fn get_router(state: Arc<State>) -> Router { pub fn get_router(state: ApiState) -> Router {
Router::new() Router::new()
.route("/element", put(create_element)) .route("/element", put(create_element))
.route("/element/:id", get(get_element).post(set_element).delete(remove_element)) .route("/element/:id", get(get_element).post(set_element).delete(remove_element))
.layer(Extension(state)) .layer(Extension(Arc::new(state)))
} }
async fn get_element(Path(id): Path<ElementId>, s: Extension<Arc<State>>) -> Response { async fn get_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
let element = s.get_element(&id); let element = s.get_element(&id);
match element { match element {
Some(el) => (StatusCode::OK, Json{0: el}).into_response(), Ok(el) => (StatusCode::OK, Json{0: el}).into_response(),
None => StatusCode::NOT_FOUND.into_response(), Err(e) => {
warn!("Element not found:\n{:?}", e);
StatusCode::NOT_FOUND.into_response()
}
} }
} }
async fn create_element(s: Extension<Arc<State>>, Json(content): Json<ElementContent>) -> Response { async fn create_element(s: Extension<Arc<ApiState>>, Json(content): Json<ElementContent>) -> Response {
let element_id = s.create_element(&content); let element_id = s.create_element(&content);
debug!("{:?}", element_id);
match element_id { match element_id {
Ok(id) => (StatusCode::OK, Json{0: &Into::<String>::into(&id)}).into_response(), Ok(id) => (StatusCode::OK, Json{0: &Into::<String>::into(&id)}).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
} }
} }
async fn set_element(Path(id): Path<ElementId>, s: Extension<Arc<State>>, Json(content): Json<ElementContent>) -> Response { async fn set_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>, Json(content): Json<ElementContent>) -> Response {
let res = s.set_element_content(&id, &content); let res = s.write_element_content(&id, &content);
match res { match res {
Ok(_) => StatusCode::OK.into_response(), Ok(_) => StatusCode::OK.into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
} }
} }
async fn remove_element(Path(id): Path<ElementId>, s: Extension<Arc<State>>) -> Response { async fn remove_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
let res = s.remove_element(&id); let res = s.remove_element(&id);
match res { match res {
Ok(_) => StatusCode::OK.into_response(), Ok(_) => StatusCode::OK.into_response(),

View file

@ -1,24 +1,22 @@
use std::sync::Arc;
use tracing::debug; use tracing::debug;
use crate::state::{State, types::PeerId}; use crate::state::{types::PeerId, CommState};
use super::{messages::{Message, MessageContent}, Peer}; use super::messages::{Message, MessageContent};
pub fn handle(state: Arc<State>, peer: &PeerId, message: Message) { pub fn handle(state: &CommState, peer: &PeerId, message: Message) {
debug!("Handling message now: {:?}", message); debug!("Handling message now: {:?}", message);
match message.content() { match message.content() {
MessageContent::Hello { peer_name } => { MessageContent::Hello { peer_name } => {
state.set_peer(&Peer::new(peer.clone(), peer_name.clone())).expect("Couldn't set peer"); state.set_peer(peer, peer_name).expect("State failed");
}, },
MessageContent::CreateElement { id, content } => { MessageContent::CreateElement { id, content } => {
state.set_element(id, content).expect("State failed"); state.add_received_element(id, content, message.id()).expect("State failed");
}, },
MessageContent::SetElement { id, content } => { MessageContent::SetElement { id, content } => {
state.set_element(id, content).expect("State failed"); state.update_element_content(id, content, message.id()).expect("State failed");
}, },
MessageContent::RemoveElement { id } => { MessageContent::RemoveElement { id } => {
state.remove_element(id).expect("State failed"); state.remove_element(id).expect("State failed");

View file

@ -5,6 +5,7 @@ use tracing::{warn, error, debug};
pub use types::*; pub use types::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use std::io::{Read, Write}; use std::io::{Read, Write};
@ -15,13 +16,13 @@ use tokio::sync::RwLock;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use crate::Config; use crate::Config;
use crate::state::State; use crate::state::CommState;
use crate::state::types::PeerId; use crate::state::types::PeerId;
use self::messages::Message; use self::messages::Message;
pub struct CommHandle { pub struct CommHandle {
state: Arc<State>, state: Arc<CommState>,
i2p_server: Arc<I2pListener>, i2p_server: Arc<I2pListener>,
// Maps peer addresses to existing connections to them // Maps peer addresses to existing connections to them
clients: Arc<RwLock<HashMap<I2pSocketAddr, Arc<RwLock<I2pStream>>>>>, clients: Arc<RwLock<HashMap<I2pSocketAddr, Arc<RwLock<I2pStream>>>>>,
@ -29,7 +30,7 @@ pub struct CommHandle {
} }
impl CommHandle { impl CommHandle {
pub fn new(state: Arc<State>, config: &Config) -> anyhow::Result<Self> { pub fn new(state: CommState, config: &Config) -> anyhow::Result<Self> {
let mut listener_builder = I2pListenerBuilder::default() let mut listener_builder = I2pListenerBuilder::default()
.with_options(SAMOptions::default()); .with_options(SAMOptions::default());
@ -42,7 +43,7 @@ impl CommHandle {
.unwrap(); .unwrap();
Ok(CommHandle { Ok(CommHandle {
state: state, state: Arc::new(state),
i2p_server: Arc::new(listener), i2p_server: Arc::new(listener),
clients: Default::default(), clients: Default::default(),
thread: RwLock::new(None), thread: RwLock::new(None),
@ -155,7 +156,7 @@ impl CommHandle {
Ok(i2p_dest) Ok(i2p_dest)
} }
fn read_connection(wrapped_stream: Arc<RwLock<I2pStream>>, state: Arc<State>) -> JoinHandle<()> { fn read_connection(wrapped_stream: Arc<RwLock<I2pStream>>, state: Arc<CommState>) -> JoinHandle<()> {
tokio::spawn(async move { tokio::spawn(async move {
let mut stream = wrapped_stream.write().await; let mut stream = wrapped_stream.write().await;
let peer: PeerId = stream.peer_addr().expect("Failed to get peer addr").into(); let peer: PeerId = stream.peer_addr().expect("Failed to get peer addr").into();
@ -172,7 +173,7 @@ impl CommHandle {
Ok(value) => { Ok(value) => {
match serde_json::from_value::<Message>(value) { match serde_json::from_value::<Message>(value) {
Ok(message) => { Ok(message) => {
message_processor::handle(state.clone(), &peer, message); message_processor::handle(state.deref(), &peer, message);
}, },
Err(e) => warn!("Deserialization failed: {:?}", e), Err(e) => warn!("Deserialization failed: {:?}", e),
} }
@ -200,14 +201,14 @@ mod tests {
use i2p::sam_options::SAMOptions; use i2p::sam_options::SAMOptions;
use crate::Config; use crate::Config;
use crate::state::State; use crate::state::{State, CommState};
use crate::comm::{messages, Message}; use crate::comm::{messages, Message};
use crate::state::types::ElementId; use crate::state::types::ElementId;
use super::CommHandle; use super::CommHandle;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
pub async fn msg() { pub async fn msg() {
let ch = CommHandle::new(State::new().await.unwrap(), &Config::default() ).unwrap(); let ch = CommHandle::new(CommState::new(State::new().await.unwrap()), &Config::default() ).unwrap();
ch.run().await; ch.run().await;
println!("My address: {:?}", ch.i2p_b32_address()); println!("My address: {:?}", ch.i2p_b32_address());

View file

@ -6,7 +6,7 @@ use comm::{CommHandle, Peer};
use config::Config; use config::Config;
use i2p::net::I2pSocketAddr; use i2p::net::I2pSocketAddr;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use state::{State, types::{ElementContent, ElementId, MessageId, PeerId}}; use state::{State, types::{ElementContent, ElementId, MessageId, PeerId}, CommState, ApiState};
mod api; mod api;
pub mod comm; pub mod comm;
@ -28,10 +28,10 @@ pub struct Ubisync {
impl Ubisync { impl Ubisync {
pub async fn new(config: &Config) -> anyhow::Result<Self> { pub async fn new(config: &Config) -> anyhow::Result<Self> {
let state = State::new().await?; let state = State::new().await?;
let comm_handle = Arc::new(CommHandle::new(state.clone(), config)?); let comm_handle = Arc::new(CommHandle::new(CommState::new(state.clone()), config)?);
state.set_comm_handle(comm_handle.clone()); state.set_comm_handle(comm_handle.clone());
let api = Arc::new(ApiBuilder::from(config.api_config.clone()).build(state.clone()).await); let api = Arc::new(ApiBuilder::from(config.api_config.clone()).build(ApiState::new(state.clone())).await);
comm_handle.run().await; comm_handle.run().await;
Ok(Ubisync { Ok(Ubisync {
@ -64,10 +64,6 @@ impl Ubisync {
pub fn get_destination(&self) -> anyhow::Result<I2pSocketAddr> { pub fn get_destination(&self) -> anyhow::Result<I2pSocketAddr> {
self.comm_handle.i2p_address() self.comm_handle.i2p_address()
} }
pub fn create_element(&self, content: &ElementContent) -> anyhow::Result<ElementId> {
self.state_handle.create_element(content)
}
} }

86
src/state/api_state.rs Normal file
View file

@ -0,0 +1,86 @@
use std::sync::Arc;
use cozo::DbInstance;
use tracing::debug;
use crate::{state::{types::ElementId, queries}, comm::messages::MessageContent};
use super::{State, types::{ElementContent, Element}};
pub struct ApiState {
state: Arc<State>,
}
impl ApiState {
pub fn new(state: Arc<State>) -> Self {
ApiState { state: state }
}
pub fn create_element(&self, content: &ElementContent) -> anyhow::Result<ElementId> {
let id = ElementId::new();
queries::elements::add(self.db(), &id, &content, None, true)?;
debug!("Added element {{{}}}", &id.to_string());
self.state.send_to_peers(MessageContent::CreateElement { id: id.clone(), content: content.clone() });
Ok(id)
}
pub fn write_element_content(&self, id: &ElementId, content: &ElementContent) -> anyhow::Result<()> {
queries::elements::set_content(self.db(), id, content)?;
queries::elements::set_local_changes(self.db(), id, true)?;
debug!("Wrote element content {{{}}}", &id.to_string());
Ok(())
}
pub fn remove_element(&self, id: &ElementId) -> anyhow::Result<()> {
let res = self.state.remove_element(id);
debug!("Removed element {{{}}}", &id.to_string());
res
}
pub fn get_element(&self, id: &ElementId) -> anyhow::Result<Element> {
self.state.get_element(id)
}
fn db(&self) -> &DbInstance {
&self.state.db
}
}
#[cfg(test)]
mod tests {
use super::ApiState;
use crate::state::{types::ElementContent, State};
#[tokio::test]
#[serial_test::serial]
async fn test_element_create() {
tracing_subscriber::fmt().pretty().init();
let state = ApiState::new(State::new().await.unwrap());
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
let el = state.get_element(&id).unwrap();
assert_eq!(
ElementContent::Text("Test-text".to_string()),
el.content().to_owned()
)
}
#[tokio::test]
#[serial_test::serial]
async fn test_element_write() {
tracing_subscriber::fmt().pretty().init();
let state = ApiState::new(State::new().await.unwrap());
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
state.write_element_content(&id,&ElementContent::Text("Test-text 2".to_string())).unwrap();
let el = state.get_element(&id).unwrap();
assert_eq!(
ElementContent::Text("Test-text 2".to_string()),
el.content().to_owned()
)
}
}

98
src/state/comm_state.rs Normal file
View file

@ -0,0 +1,98 @@
use std::sync::Arc;
use cozo::DbInstance;
use tracing::debug;
use crate::{state::queries, comm::Peer};
use super::{State, types::{MessageId, ElementContent, ElementId, PeerId, Element}};
//TODO: Notify API about changes
pub struct CommState {
state: Arc<State>
}
impl CommState {
pub fn new(state: Arc<State>) -> Self {
CommState { state: state }
}
pub fn add_received_element(&self, id: &ElementId, content: &ElementContent, latest_message: &MessageId) -> anyhow::Result<()> {
queries::elements::add(self.db(), &id, &content, Some(latest_message.to_owned()), false)?;
debug!("Added element {{{}}}", &id.to_string());
Ok(())
}
pub fn update_element_content(&self, id: &ElementId, content: &ElementContent, latest_message: &MessageId) -> anyhow::Result<()> {
//TODO: resolve potential conflicts with local changes
queries::elements::set_content(self.db(), id, content)?;
queries::elements::set_latest_message(self.db(), id, Some(latest_message.to_owned()))?;
debug!("Updated element {{{}}}", &id.to_string());
Ok(())
}
pub fn remove_element(&self, id: &ElementId) -> anyhow::Result<()> {
let res = self.state.remove_element(id);
debug!("Removed element {{{}}}", &id.to_string());
res
}
pub fn get_element(&self, id: &ElementId) -> anyhow::Result<Element> {
self.state.get_element(id)
}
pub fn set_peer(&self, id: &PeerId, name: &str) -> anyhow::Result<()> {
queries::peers::put(self.db(), id, name)?;
debug!("Set peer {{{}}}", id.to_string());
Ok(())
}
pub fn get_peers(&self) -> anyhow::Result<Vec<Peer>> {
self.state.get_peers()
}
fn db(&self) -> &DbInstance {
&self.state.db
}
}
#[cfg(test)]
mod tests {
use super::CommState;
use crate::state::{types::{ElementContent, ElementId, MessageId}, State};
#[tokio::test]
#[serial_test::serial]
async fn test_element_add() {
tracing_subscriber::fmt().pretty().init();
let state = CommState::new(State::new().await.unwrap());
let id = ElementId::new();
state.add_received_element(&id, &ElementContent::Text("Test-text".to_string()), &MessageId::new()).unwrap();
let el = state.get_element(&id).unwrap();
assert_eq!(
ElementContent::Text("Test-text".to_string()),
el.content().to_owned()
)
}
#[tokio::test]
#[serial_test::serial]
async fn test_element_update() {
tracing_subscriber::fmt().pretty().init();
let state = CommState::new(State::new().await.unwrap());
let id = ElementId::new();
state.add_received_element(&id, &ElementContent::Text("Test-text".to_string()), &MessageId::new()).unwrap();
state.update_element_content(&id,&ElementContent::Text("Test-text 2".to_string()), &MessageId::new()).unwrap();
let el = state.get_element(&id).unwrap();
assert_eq!(
ElementContent::Text("Test-text 2".to_string()),
el.content().to_owned()
)
}
}

View file

@ -10,9 +10,14 @@ use self::types::{ElementContent, ElementId, Element, Tag};
pub mod types; pub mod types;
mod api_state;
mod comm_state;
mod queries; mod queries;
mod schema; mod schema;
pub use api_state::ApiState;
pub use comm_state::CommState;
pub struct State { pub struct State {
db: DbInstance, db: DbInstance,
comm_handle: RwLock<Option<Arc<CommHandle>>>, comm_handle: RwLock<Option<Arc<CommHandle>>>,
@ -34,58 +39,37 @@ impl State {
*self.comm_handle.write().as_deref_mut().expect("Could not set state's CommHandle") = Some(handle); *self.comm_handle.write().as_deref_mut().expect("Could not set state's CommHandle") = Some(handle);
} }
// Create an element and add it to the database
pub fn create_element(&self, content: &ElementContent) -> anyhow::Result<ElementId> {
let id = ElementId::new();
queries::set_element(&self.db, &id, &content)?;
debug!("Created element with id {:?}: {:?}", &id, self.get_element(&id));
self.send_to_peers(MessageContent::CreateElement { id: id.clone(), content: content.clone() });
Ok(id)
}
// Anyone updated an element, update it in the database
pub fn set_element(&self, element_id: &ElementId, content: &ElementContent) -> anyhow::Result<()> {
let res = queries::set_element(&self.db, element_id, content);
debug!("Set element with id {:?}: {:?}", element_id, self.get_element(element_id));
self.send_to_peers(MessageContent::SetElement { id: element_id.clone(), content: content.clone() });
res
}
pub fn set_element_content(&self, element_id: &ElementId, content: &ElementContent) -> anyhow::Result<()> { pub fn set_element_content(&self, element_id: &ElementId, content: &ElementContent) -> anyhow::Result<()> {
let res = queries::set_element_content(&self.db, element_id, content); let res = queries::elements::set_content(&self.db, element_id, content);
debug!("Set element content with id {:?}: {:?}", element_id, self.get_element(element_id)); debug!("Set content of element with id {:?}: {:?}", element_id, self.get_element(element_id));
self.send_to_peers(MessageContent::SetElement { id: element_id.clone(), content: content.clone() }); self.send_to_peers(MessageContent::SetElement { id: element_id.clone(), content: content.clone() });
res res
} }
pub fn remove_element(&self, element_id: &ElementId) -> anyhow::Result<()> { pub fn remove_element(&self, element_id: &ElementId) -> anyhow::Result<()> {
let res = queries::remove_element(&self.db, element_id); let res = queries::elements::remove(&self.db, element_id);
self.send_to_peers(MessageContent::RemoveElement { id: element_id.clone() }); self.send_to_peers(MessageContent::RemoveElement { id: element_id.clone() });
res res
} }
pub fn get_element(&self, id: &ElementId) -> Option<Element> { pub fn get_element(&self, id: &ElementId) -> anyhow::Result<Element> {
queries::get_element(&self.db, id).ok() queries::elements::get(&self.db, id)
} }
pub fn get_elements_by_tag(&self, tag: &Tag) -> Vec<ElementId> { pub fn get_elements_by_tag(&self, tag: &Tag) -> Vec<ElementId> {
queries::get_elements_by_tag(&self.db, tag) queries::elements::get_by_tag(&self.db, tag)
.map_err(|e| {error!("{}", e); e}) .map_err(|e| {error!("{}", e); e})
.unwrap_or(vec![]) .unwrap_or(vec![])
} }
pub fn set_peer(&self, peer: &Peer) -> anyhow::Result<()> { pub fn set_peer(&self, peer: &Peer) -> anyhow::Result<()> {
queries::add_peer(&self.db, &peer.id(), &peer.name()) queries::peers::put(&self.db, &peer.id(), &peer.name())
} }
pub fn get_peers(&self) -> anyhow::Result<Vec<Peer>> { pub fn get_peers(&self) -> anyhow::Result<Vec<Peer>> {
queries::get_peers(&self.db) queries::peers::get(&self.db)
} }
@ -104,37 +88,3 @@ impl State {
} }
} }
} }
#[cfg(test)]
mod tests {
use crate::state::State;
use crate::state::types::ElementContent;
#[tokio::test]
#[serial_test::serial]
async fn test_create() {
tracing_subscriber::fmt().pretty().init();
let state = State::new().await.unwrap();
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
let el = state.get_element(&id).unwrap();
assert_eq!(
ElementContent::Text("Test-text".to_string()),
el.content().to_owned()
)
}
#[tokio::test]
#[serial_test::serial]
async fn test_update() {
tracing_subscriber::fmt().pretty().init();
let state = State::new().await.unwrap();
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
state.set_element(&id,&ElementContent::Text("Test-text 2".to_string())).unwrap();
let el = state.get_element(&id).unwrap();
assert_eq!(
ElementContent::Text("Test-text 2".to_string()),
el.content().to_owned()
)
}
}

View file

@ -3,65 +3,82 @@ use std::collections::BTreeMap;
use anyhow::{Error, bail}; use anyhow::{Error, bail};
use cozo::{DbInstance, DataValue, JsonData, ScriptMutability}; use cozo::{DbInstance, DataValue, JsonData, ScriptMutability};
use serde_json::Value; use serde_json::Value;
use tracing::error; use tracing::{error, debug};
use crate::{state::{ElementContent, ElementId, Element, types::Tag}, run_query}; use crate::{state::{ElementContent, ElementId, Element, types::{Tag, MessageId}}, run_query};
pub fn set_element(db: &DbInstance, id: &ElementId, content: &ElementContent) -> anyhow::Result<()> { pub fn add(db: &DbInstance, id: &ElementId, content: &ElementContent, latest_message: Option<MessageId>, local_changes: bool) -> anyhow::Result<()> {
let params = vec![ let params = vec![
("id", DataValue::Str(serde_json::to_string(&id)?.into())), ("id", DataValue::Str(serde_json::to_string(&id)?.into())),
("content", DataValue::Str(serde_json::to_string(content)?.into())) ("content", DataValue::Json(JsonData(serde_json::to_value(content)?))),
("latest_message", match latest_message {
Some(m) => DataValue::Str(serde_json::to_string(&m)?.into()),
None => DataValue::Null,
}),
("local_changes", DataValue::Bool(local_changes)),
]; ];
match run_query!(&db, ":put elements {id => content}", params, ScriptMutability::Mutable) { match run_query!(&db, ":insert elements {id => content, latest_message, local_changes}", params, ScriptMutability::Mutable) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(report) => bail!(report), Err(report) => bail!(report),
} }
} }
pub fn set_element_content(db: &DbInstance, id: &ElementId, content: &ElementContent) -> anyhow::Result<()> { pub fn set_content(db: &DbInstance, id: &ElementId, content: &ElementContent) -> anyhow::Result<()> {
let params = vec![ set_property(db, id, "content", DataValue::Json(JsonData(serde_json::to_value(content)?)))
("id", DataValue::Str(serde_json::to_string(&id)?.into())),
("content", DataValue::Str(serde_json::to_string(content)?.into()))
];
match run_query!(&db, ":put elements {id => content}", params, ScriptMutability::Mutable) {
Ok(_) => Ok(()),
Err(report) => bail!(report),
}
} }
pub fn remove_element(db: &DbInstance, id: &ElementId) -> anyhow::Result<()> { pub fn set_latest_message(db: &DbInstance, id: &ElementId, latest_message: Option<MessageId>) -> anyhow::Result<()> {
set_property(db, id, "latest_message", match latest_message {
Some(m) => DataValue::Str(serde_json::to_string(&m)?.into()),
None => DataValue::Null,
})
}
pub fn set_local_changes(db: &DbInstance, id: &ElementId, local_changes: bool) -> anyhow::Result<()> {
set_property(db, id, "local_changes", DataValue::Bool(local_changes))
}
pub fn remove(db: &DbInstance, id: &ElementId) -> anyhow::Result<()> {
match run_query!(&db, ":delete elements {id}", vec![("id", DataValue::Str(serde_json::to_string(&id)?.into()))], cozo::ScriptMutability::Mutable) { match run_query!(&db, ":delete elements {id}", vec![("id", DataValue::Str(serde_json::to_string(&id)?.into()))], cozo::ScriptMutability::Mutable) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(report) => bail!(report), Err(report) => bail!(report),
} }
} }
pub fn get_element(db: &DbInstance, id: &ElementId) -> anyhow::Result<Element> { pub fn get(db: &DbInstance, id: &ElementId) -> anyhow::Result<Element> {
let mut params = BTreeMap::new(); let mut params = BTreeMap::new();
params.insert("id".to_string(), DataValue::Str(serde_json::to_string(&id)?.into())); params.insert("id".to_string(), DataValue::Str(serde_json::to_string(&id)?.into()));
let result = db.run_script(" let result = db.run_script("
?[content] := *elements[$id, content] ?[content, latest_message, local_changes] := *elements[$id, content, latest_message, local_changes]
", params, cozo::ScriptMutability::Immutable); ", params, cozo::ScriptMutability::Immutable);
match result { match result {
Ok(val) => { Ok(val) => {
if let Some(firstrow) = val.rows.first() { if let Some(firstrow) = val.rows.first() {
if let [ DataValue::Json(JsonData(Value::String(content))) ] = firstrow.as_slice() { debug!("db result: {:?}", &firstrow.as_slice());
return Ok(Element::new( if let [ DataValue::Json(JsonData(content)), latest_message, DataValue::Bool(local_changes) ] = firstrow.as_slice() {
return Ok(Element::from((
id.to_owned(), id.to_owned(),
content.as_str().try_into()?, serde_json::from_value(content.to_owned())?,
)); match latest_message {
DataValue::Str(s) => Some(serde_json::from_str(s)?),
_ => None,
},
local_changes.to_owned()
)));
} }
return Err(Error::msg("Could not parse db result as Element"));
}
else {
return Err(Error::msg("No rows returned for element query"))
} }
return Err(Error::msg("Could not parse db result as Element"));
}, },
Err(report) => bail!(report), Err(report) => bail!(report),
} }
} }
pub fn get_elements_by_tag(db: &DbInstance, tag: &Tag) -> anyhow::Result<Vec<ElementId>> { pub fn get_by_tag(db: &DbInstance, tag: &Tag) -> anyhow::Result<Vec<ElementId>> {
let mut params = BTreeMap::new(); let mut params = BTreeMap::new();
params.insert("tag".to_string(), DataValue::Str(serde_json::to_string(tag)?.into())); params.insert("tag".to_string(), DataValue::Str(serde_json::to_string(tag)?.into()));
@ -88,3 +105,16 @@ pub fn get_elements_by_tag(db: &DbInstance, tag: &Tag) -> anyhow::Result<Vec<Ele
Err(report) => bail!(report), Err(report) => bail!(report),
} }
} }
fn set_property(db: &DbInstance, id: &ElementId, key: &str, value: DataValue) -> anyhow::Result<()> {
let params = vec![
("id", DataValue::Str(serde_json::to_string(id)?.into())),
(key, value)
];
match run_query!(&db, format!(":update elements {{id => {key}}}"), params, ScriptMutability::Mutable) {
Ok(_) => Ok(()),
Err(report) => bail!(report),
}
}

View file

@ -1,9 +1,6 @@
mod elements; pub mod elements;
pub use elements::*; pub mod peers;
mod peers;
pub use peers::*;
#[macro_export] #[macro_export]

View file

@ -7,7 +7,7 @@ use crate::{state::types::PeerId, comm::Peer, run_query};
pub fn add_peer(db: &DbInstance, id: &PeerId, name: &str) -> anyhow::Result<()> { pub fn put(db: &DbInstance, id: &PeerId, name: &str) -> anyhow::Result<()> {
let params = vec![ let params = vec![
("id", DataValue::Str(serde_json::to_string(id)?.into())), ("id", DataValue::Str(serde_json::to_string(id)?.into())),
("name", DataValue::Str(serde_json::to_string(name)?.into())) ("name", DataValue::Str(serde_json::to_string(name)?.into()))
@ -19,7 +19,7 @@ pub fn add_peer(db: &DbInstance, id: &PeerId, name: &str) -> anyhow::Result<()>
} }
} }
pub fn get_peers(db: &DbInstance) -> anyhow::Result<Vec<Peer>> { pub fn get(db: &DbInstance) -> anyhow::Result<Vec<Peer>> {
let result = db.run_script(" let result = db.run_script("
?[id, name] := *peers{id, name} ?[id, name] := *peers{id, name}
", Default::default(), cozo::ScriptMutability::Immutable); ", Default::default(), cozo::ScriptMutability::Immutable);

View file

@ -17,10 +17,12 @@ pub fn add_schema(db: &DbInstance) -> anyhow::Result<()> {
id: String, id: String,
=> =>
content: Json, content: Json,
latest_message: String?,
local_changes: Bool,
}} }}
{:create tags { {:create tags {
tag: String, tag: String,
element: String element: String,
}} }}
", params, cozo::ScriptMutability::Mutable) { ", params, cozo::ScriptMutability::Mutable) {
Ok(_) => Ok(()), Ok(_) => Ok(()),

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use super::{ElementId, ElementContent}; use super::{ElementId, ElementContent, MessageId};
@ -8,12 +8,21 @@ use super::{ElementId, ElementContent};
pub struct Element { pub struct Element {
// Uuid identifying the element itself // Uuid identifying the element itself
id: ElementId, id: ElementId,
content: ElementContent content: ElementContent,
latest_message: Option<MessageId>,
local_changes: bool,
}
impl From<(ElementId, ElementContent, Option<MessageId>, bool)> for Element {
fn from(value: (ElementId, ElementContent, Option<MessageId>, bool)) -> Self {
Element { id: value.0, content: value.1, latest_message: value.2, local_changes: value.3 }
}
} }
impl Element { impl Element {
pub fn new(id: ElementId, content: ElementContent) -> Self { pub fn new(id: ElementId, content: ElementContent) -> Self {
Element { id: id, content: content } // A new element with no latest message must have local changes
Element { id: id, content: content, latest_message: None, local_changes: true }
} }
pub fn id(&self) -> &ElementId { pub fn id(&self) -> &ElementId {

View file

@ -9,6 +9,12 @@ impl ElementId {
} }
} }
impl ToString for ElementId {
fn to_string(&self) -> String {
self.0.to_string()
}
}
impl From<&ElementId> for String { impl From<&ElementId> for String {
fn from(value: &ElementId) -> Self { fn from(value: &ElementId) -> Self {
value.0.to_string() value.0.to_string()

View file

@ -14,6 +14,12 @@ impl PeerId {
} }
} }
impl ToString for PeerId {
fn to_string(&self) -> String {
self.i2p_addr.to_string()
}
}
impl TryFrom<&str> for PeerId { impl TryFrom<&str> for PeerId {
type Error = anyhow::Error; type Error = anyhow::Error;

View file

@ -19,12 +19,16 @@ async fn two_nodes_element_creation() {
let test_element_content = ElementContent::Text("Text".to_string()); let test_element_content = ElementContent::Text("Text".to_string());
let put_resp = http_client.put(&format!("http://localhost:9981/v0/element")).json(&test_element_content).send().await.unwrap(); let put_resp = http_client.put(&format!("http://localhost:9981/v0/element")).json(&test_element_content).send().await.unwrap();
debug!("{:?}", &put_resp); debug!("{:?}", &put_resp);
let id = serde_json::from_str::<ElementId>(&put_resp.text().await.expect("No put response body")).expect("Could not deserialize ElementId"); let put_resp_text = put_resp.text().await.expect("No put response body");
debug!("{}", put_resp_text);
let id = serde_json::from_str::<ElementId>(&put_resp_text).expect("Could not deserialize ElementId");
tokio::time::sleep(Duration::from_millis(3000)).await; tokio::time::sleep(Duration::from_millis(3000)).await;
let get_resp = http_client.get(&format!("http://localhost:9982/v0/element/{}", Into::<String>::into(&id))).send().await.expect("Get request failed"); let get_resp = http_client.get(&format!("http://localhost:9982/v0/element/{}", Into::<String>::into(&id))).send().await.expect("Get request failed");
let received_element = serde_json::from_str::<Element>(&get_resp.text().await.expect("No get request body")).expect("Could not deserialize Element"); let get_resp_text = get_resp.text().await.expect("No get request body");
debug!("{}", get_resp_text);
let received_element = serde_json::from_str::<Element>(&get_resp_text).expect("Could not deserialize Element");
debug!("Other node received this element: {:?}", received_element); debug!("Other node received this element: {:?}", received_element);
assert_eq!(&test_element_content, received_element.content()); assert_eq!(&test_element_content, received_element.content());