Added minimal sdk, simplified test using the sdk's client

This commit is contained in:
Philip (a-0) 2024-01-07 19:13:51 +01:00
parent a75c115761
commit 84784599a7
16 changed files with 379 additions and 83 deletions

10
Cargo.lock generated
View file

@ -3060,6 +3060,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"ubisync-lib",
"ubisync-sdk",
"uuid",
]
@ -3088,6 +3089,15 @@ dependencies = [
[[package]]
name = "ubisync-sdk"
version = "0.1.0"
dependencies = [
"anyhow",
"reqwest",
"serde",
"serde_json",
"tracing",
"tracing-subscriber",
"ubisync-lib",
]
[[package]]
name = "ucd-trie"

View file

@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = [
"ubisync",

View file

@ -10,6 +10,7 @@ axum = { version = "0.7.2", features = [ "macros" ] }
chrono = "0.4.31"
itertools = "0.12.0"
jsonwebtoken = "9.2.0"
reqwest = "0.11.23"
serde = { version = "1.0.166", features = [ "derive" ] }
serde_json = "1.0.99"
serde_with = "3.3.0"

View file

@ -0,0 +1,28 @@
use reqwest::Method;
use serde::{Serialize, Deserialize};
use super::UbisyncRequest;
#[derive(Serialize, Deserialize)]
pub struct AppRegisterRequest {
pub name: String,
pub description: String,
}
#[derive(Serialize, Deserialize)]
pub struct AppRegisterResponse {
pub token: String,
}
impl UbisyncRequest for AppRegisterRequest {
type PathParameters = ();
type Response = AppRegisterResponse;
fn method(&self) -> reqwest::Method {
Method::PUT
}
fn path(&self, _: Self::PathParameters) -> String {
"/app/register".to_string()
}
}

View file

@ -0,0 +1,88 @@
use reqwest::Method;
use serde::{Serialize, Deserialize};
use crate::types::{ElementContent, ElementId, Element};
use super::UbisyncRequest;
#[derive(Serialize, Deserialize)]
pub struct ElementCreateRequest {
pub content: ElementContent,
}
#[derive(Serialize, Deserialize)]
pub struct ElementCreateResponse {
pub id: ElementId,
}
impl UbisyncRequest for ElementCreateRequest {
type PathParameters = ();
type Response = ElementCreateResponse;
fn method(&self) -> Method {
Method::PUT
}
fn path(&self, _: Self::PathParameters) -> String {
"/element".to_string()
}
}
#[derive(Serialize, Deserialize)]
pub struct ElementGetRequest;
#[derive(Serialize, Deserialize)]
pub struct ElementGetResponse {
pub element: Element,
}
impl UbisyncRequest for ElementGetRequest {
type PathParameters = ElementId;
type Response = ElementGetResponse;
fn method(&self) -> Method {
Method::GET
}
fn path(&self, params: Self::PathParameters) -> String {
format!("/element/{}", params.to_string())
}
}
#[derive(Serialize, Deserialize)]
pub struct ElementSetRequest {
pub content: ElementContent,
}
#[derive(Serialize, Deserialize)]
pub struct ElementSetResponse;
impl UbisyncRequest for ElementSetRequest {
type PathParameters = ElementId;
type Response = ElementSetResponse;
fn method(&self) -> Method {
Method::POST
}
fn path(&self, params: Self::PathParameters) -> String {
format!("/element/{}", serde_json::to_string(&params).unwrap())
}
}
#[derive(Serialize, Deserialize)]
pub struct ElementRemoveRequest;
#[derive(Serialize, Deserialize)]
pub struct ElementRemoveResponse;
impl UbisyncRequest for ElementRemoveRequest {
type PathParameters = ElementId;
type Response = ElementRemoveResponse;
fn method(&self) -> Method {
Method::DELETE
}
fn path(&self, params: Self::PathParameters) -> String {
format!("/element/{}", serde_json::to_string(&params).unwrap())
}
}

View file

@ -0,0 +1,23 @@
use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};
pub mod app;
pub mod element;
/// Any struct defining a request body for the ubisync API must implement this trait
/// It is used both by the client in the SDK and by the API logic in the ubisync node
#[async_trait]
pub trait UbisyncRequest: for<'de> Deserialize<'de> + Serialize {
type PathParameters;
type Response: for<'de> Deserialize<'de> + Serialize;
fn method(&self) -> Method;
fn path(&self, params: Self::PathParameters) -> String;
async fn parse_response(resp: reqwest::Response) -> Result<Self::Response, reqwest::Error>
where
for<'de> <Self as UbisyncRequest>::Response: Deserialize<'de>,
{
resp.json().await
}
}

View file

@ -1,3 +1,4 @@
pub mod api;
pub mod messages;
pub mod types;
pub mod peer;
pub mod types;

View file

@ -6,3 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.79"
reqwest = { version = "0.11.23", features = [ "json" ] }
serde = { version = "1.0.166", features = [ "derive" ] }
serde_json = "1.0.99"
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
ubisync-lib = { path = "../ubisync-lib" }

20
ubisync-sdk/src/error.rs Normal file
View file

@ -0,0 +1,20 @@
use std::fmt::Display;
#[derive(Debug)]
pub enum UbisyncError {
InvalidNodeReply(String),
AppRegistrationFailed,
}
impl Display for UbisyncError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidNodeReply(msg) => write!(f, "Invalid reply from ubisync node: {}", msg),
Self::AppRegistrationFailed => write!(f, "Registrating this app at the ubisync node failed."),
}
}
}
impl std::error::Error for UbisyncError {}

View file

@ -0,0 +1,118 @@
use anyhow::anyhow;
use error::UbisyncError;
use reqwest::{Client, StatusCode};
use ubisync_lib::api::{
app::{AppRegisterRequest, AppRegisterResponse},
UbisyncRequest,
};
pub use ubisync_lib::*;
pub mod error;
pub struct UbisyncClient {
host: String,
port: u16,
selected_api_version: String,
base_url: String,
jwt_token: String,
reqwest_client: Client,
}
impl UbisyncClient {
pub async fn init(
host: &str,
port: u16,
jwt_token: Option<&str>,
application_name: &str,
application_description: &str,
) -> Result<Self, UbisyncError> {
let http_client = Client::new();
let mut node_api_versions = http_client
.get(&format!("http://{}:{}/versions", host, port))
.send()
.await
.expect("Failed to contact ubisync node, it may be offline.")
.json::<Vec<String>>()
.await
.expect("Failed to read ubisync node's available API versions.");
node_api_versions.sort();
let selected_version = node_api_versions
.get(0)
.expect("No available API version returned by ubisync node");
let token = match jwt_token {
Some(t) => t.to_string(),
None => {
let response = http_client
.put(Self::build_base_url(host, port, &selected_version) + "/app/register")
.json(&AppRegisterRequest {
name: application_name.to_string(),
description: application_description.to_string(),
})
.send()
.await
.expect("App registration request failed.");
if response.status() != StatusCode::OK {
return Err(UbisyncError::AppRegistrationFailed);
}
response
.json::<AppRegisterResponse>()
.await
.expect("Failed to extract JWT from app regstration request")
.token
}
};
Ok(UbisyncClient {
host: host.to_string(),
port: port,
selected_api_version: selected_version.to_string(),
base_url: Self::build_base_url(host, port, selected_version),
jwt_token: token.to_string(),
reqwest_client: http_client,
})
}
pub async fn send<R>(
&self,
request: R,
parameters: R::PathParameters,
) -> anyhow::Result<R::Response>
where
R: UbisyncRequest,
{
self.reqwest_client
.request(
request.method(),
&(self.base_url.to_owned() + &request.path(parameters)),
)
.bearer_auth(&self.jwt_token)
.json(&request)
.send()
.await
.map_err(|e| anyhow!(e))?
.json::<R::Response>()
.await
.map_err(|e| anyhow!(e))
}
pub async fn set_host(&mut self, host: String) {
self.host = host;
self.base_url = Self::build_base_url(&self.host, self.port, &self.selected_api_version);
}
pub async fn set_port(&mut self, port: u16) {
self.port = port;
self.base_url = Self::build_base_url(&self.host, self.port, &self.selected_api_version);
}
pub async fn set_api_version(&mut self, version: String) {
self.selected_api_version = version;
self.base_url = Self::build_base_url(&self.host, self.port, &self.selected_api_version);
}
fn build_base_url(host: &str, port: u16, api_version: &str) -> String {
format!("http://{}:{}/{}", host, port, api_version)
}
}

View file

@ -26,3 +26,4 @@ ubisync-lib = { path = "../ubisync-lib" }
[dev-dependencies]
reqwest = { version = "0.11.20", features = [ "json" ] }
ubisync-sdk = { path = "../ubisync-sdk" }

View file

@ -1,4 +1,4 @@
use axum::Router;
use axum::{Router, routing::get, response::{Response, IntoResponse}, http::StatusCode, Json};
use tokio::{net::TcpListener, task::JoinHandle};
use crate::{config::ApiConfig, state::ApiState};
@ -44,6 +44,8 @@ impl From<ApiConfig> for ApiBuilder {
impl ApiBuilder {
pub async fn build(&self, state: ApiState) -> Api {
let mut app: Router = Router::new();
app = app.route("/versions", get(list_available_versions));
match &self.version {
Some(v) if v == "v0" => app = app.nest(&format!("/{}", v), v0::get_router(state)),
_ => app = app.nest("/v0", v0::get_router(state)),
@ -72,3 +74,8 @@ impl ApiBuilder {
}
}
}
async fn list_available_versions() -> Response {
(StatusCode::OK, Json {0: vec!["v0"]}).into_response()
}

View file

@ -11,11 +11,11 @@ use axum::{
use jsonwebtoken::{decode, Header};
use serde::{Deserialize, Serialize};
use tracing::{debug, error, warn};
use ubisync_lib::api::app::{AppRegisterRequest, AppRegisterResponse};
use uuid::Uuid;
use crate::state::ApiState;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AppId(Uuid);
@ -30,15 +30,6 @@ struct JWTClaims {
sub: AppId,
}
#[derive(Serialize, Deserialize)]
pub struct AppDescription {
pub name: String,
pub desc_text: String,
}
pub(super) async fn auth(
s: Extension<Arc<ApiState>>,
mut request: Request<Body>,
@ -67,12 +58,12 @@ pub(super) async fn auth(
pub(super) async fn register(
s: Extension<Arc<ApiState>>,
Json(data): Json<AppDescription>,
Json(body): Json<AppRegisterRequest>,
) -> Response {
// Maybe ask for consent by user
// If user wants registration, proceed
let result = s.add_app(&data);
let result = s.add_app(&body.name, &body.description);
match result {
Ok(id) => {
@ -83,7 +74,13 @@ pub(super) async fn register(
&s.jwt_encoding_key(),
);
match jwt {
Ok(token) => (StatusCode::OK, token).into_response(),
Ok(token) => (
StatusCode::OK,
Json {
0: AppRegisterResponse { token: token },
},
)
.into_response(),
Err(e) => {
warn!("Failed to encode token: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()

View file

@ -6,17 +6,28 @@ use axum::{
response::{IntoResponse, Response},
Extension,
};
use tracing::{debug, warn};
use tracing::debug;
use crate::state::ApiState;
use ubisync_lib::types::{ElementContent, ElementId};
use ubisync_lib::{
api::element::{
ElementCreateRequest, ElementCreateResponse, ElementGetResponse, ElementSetRequest,
ElementSetResponse, ElementRemoveResponse,
},
types::ElementId,
};
pub(super) async fn get(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
let element = s.get_element(&id);
match element {
Ok(el) => (StatusCode::OK, Json { 0: el }).into_response(),
Err(e) => {
warn!("Element not found:\n{:?}", e);
Ok(el) => (
StatusCode::OK,
Json {
0: ElementGetResponse { element: el },
},
)
.into_response(),
Err(_) => {
StatusCode::NOT_FOUND.into_response()
}
}
@ -24,15 +35,15 @@ pub(super) async fn get(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>)
pub(super) async fn create(
s: Extension<Arc<ApiState>>,
Json(content): Json<ElementContent>,
Json(req): Json<ElementCreateRequest>,
) -> Response {
let element_id = s.create_element(&content);
let element_id = s.create_element(&req.content);
debug!("{:?}", element_id);
match element_id {
Ok(id) => (
StatusCode::OK,
Json {
0: &Into::<String>::into(&id),
0: ElementCreateResponse { id },
},
)
.into_response(),
@ -43,11 +54,17 @@ pub(super) async fn create(
pub(super) async fn set(
Path(id): Path<ElementId>,
s: Extension<Arc<ApiState>>,
Json(content): Json<ElementContent>,
Json(req): Json<ElementSetRequest>,
) -> Response {
let res = s.write_element_content(&id, &content);
let res = s.write_element_content(&id, &req.content);
match res {
Ok(_) => StatusCode::OK.into_response(),
Ok(_) => (
StatusCode::OK,
Json {
0: ElementSetResponse,
},
)
.into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
@ -55,7 +72,7 @@ pub(super) async fn set(
pub(super) async fn remove(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
let res = s.remove_element(&id);
match res {
Ok(_) => StatusCode::OK.into_response(),
Ok(_) => (StatusCode::OK, Json { 0: ElementRemoveResponse }).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

View file

@ -6,7 +6,7 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Validation};
use tracing::debug;
use ubisync_lib::{types::{ElementContent, ElementId, Element}, messages::MessageContent};
use crate::{api::v0::app::{AppDescription, AppId}, state::queries};
use crate::{api::v0::app::AppId, state::queries};
use super::State;
@ -29,15 +29,15 @@ impl ApiState {
}
}
pub fn add_app(&self, description: &AppDescription) -> anyhow::Result<AppId> {
pub fn add_app(&self, name: &str, description: &str) -> anyhow::Result<AppId> {
let id = AppId::new();
let last_access = Utc::now();
queries::apps::add(
self.db(),
&id,
&last_access,
&description.name,
&description.desc_text,
name,
description,
)?;
debug!("Successfully added app");

View file

@ -1,9 +1,12 @@
use std::time::Duration;
use tracing::{debug, Level};
use ubisync::{config::Config, Ubisync, api::v0::app::AppDescription};
use ubisync_lib::types::{ElementContent, ElementId, Element};
use tracing::{debug, warn, Level};
use ubisync::{config::Config, Ubisync};
use ubisync_lib::{
api::element::{ElementCreateRequest, ElementGetRequest},
types::{Element, ElementContent},
};
use ubisync_sdk::UbisyncClient;
#[tokio::test(flavor = "multi_thread")]
async fn two_nodes_element_creation() {
@ -11,6 +14,7 @@ async fn two_nodes_element_creation() {
.pretty()
.with_max_level(Level::DEBUG)
.init();
// Two nodes need to bind to different ports
let mut c2 = Config::default();
c2.api_config.port = Some(9982);
@ -19,63 +23,35 @@ async fn two_nodes_element_creation() {
ubi1.add_peer_from_id(ubi2.get_destination().unwrap().into())
.unwrap();
let http_client = reqwest::Client::new();
let register_response = http_client
.put("http://localhost:9981/v0/app/register")
.json(&AppDescription {
name: "Test".to_string(),
desc_text: "desc".to_string(),
})
.send()
let api_client1 = UbisyncClient::init("localhost", 9981, None, "App", "Long desc")
.await
.unwrap();
let jwt1 = register_response
.text()
.await
.expect("Couldn't fetch token from response");
let register_response = http_client
.put("http://localhost:9982/v0/app/register")
.json(&AppDescription {
name: "Test".to_string(),
desc_text: "desc".to_string(),
})
.send()
let api_client2 = UbisyncClient::init("localhost", 9982, None, "App", "Long desc")
.await
.unwrap();
let jwt2 = register_response
.text()
.await
.expect("Couldn't fetch token from response");
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)
.header("Authorization", &format!("Bearer {}", &jwt1))
.send()
let create_resp = api_client1
.send(
ElementCreateRequest {
content: test_element_content.clone(),
},
(),
)
.await
.unwrap();
debug!("{:?}", &put_resp);
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");
let id = create_resp.id;
tokio::time::sleep(Duration::from_millis(3000)).await;
tokio::time::sleep(Duration::from_millis(1000)).await;
let get_resp = http_client
.get(&format!(
"http://localhost:9982/v0/element/{}",
Into::<String>::into(&id)
))
.header("Authorization", &format!("Bearer {}", &jwt2))
.send()
.await
.expect("Get request failed");
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");
let mut get_resp = api_client2.send(ElementGetRequest {}, id.clone()).await;
while let Err(_) = get_resp {
warn!("Sleeping for another second, element has not arrived yet");
tokio::time::sleep(Duration::from_millis(1000)).await;
get_resp = api_client2.send(ElementGetRequest {}, id.clone()).await;
}
let received_element: Element = get_resp.unwrap().element;
debug!("Other node received this element: {:?}", received_element);
assert_eq!(&test_element_content, received_element.content());