Skip to content

Commit

Permalink
Extract WebAPI client to separate package
Browse files Browse the repository at this point in the history
  • Loading branch information
mrozycki committed Dec 13, 2023
1 parent 7e5d7e5 commit 9dd7686
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 299 deletions.
15 changes: 14 additions & 1 deletion Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"animator",
"lightfx",
"webapi",
"webapi-client",
"webui",
"visualizer",
"events",
Expand Down
6 changes: 5 additions & 1 deletion visualizer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2021"

[dependencies]
rustmas-webapi-client = { path = "../webapi-client", features = ["visualizer"] }

clap = { version = "4.4.11", features = ["derive"] }
ewebsock = "0.4.0"
itertools = "0.12.0"
reqwest = { version = "0.11.22", features = ["json", "blocking"] }
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.108"
url = "2.5.0"
Expand All @@ -27,3 +28,6 @@ features = [
"zstd",
"webgl2",
]

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.35.0", features = ["rt", "rt-multi-thread"] }
36 changes: 8 additions & 28 deletions visualizer/src/bin/visualizer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use clap::Parser;
use serde::Deserialize;
use rustmas_webapi_client::RustmasApiClient;
use url::Url;

/// Visualizer for Rustmas animations
Expand All @@ -11,35 +11,15 @@ struct Args {
endpoint: Url,
}

fn get_frames_url(endpoint: &Url) -> Url {
let mut endpoint = endpoint.clone();
endpoint.set_scheme("ws").unwrap();
endpoint.join("frames").unwrap()
}

#[derive(Deserialize)]
struct GetPointsResponse {
points: Vec<(f32, f32, f32)>,
}

fn get_points(endpoint: &Url) -> Vec<(f32, f32, f32)> {
let endpoint = {
let mut endpoint = endpoint.clone();
endpoint.set_scheme("http").unwrap();
endpoint.join("points").unwrap()
};

reqwest::blocking::get(endpoint)
.unwrap()
.json::<GetPointsResponse>()
.unwrap()
.points
}

fn main() {
let endpoint = Args::parse().endpoint;
let frames_endpoint = get_frames_url(&endpoint);
let points = get_points(&endpoint);
let api = RustmasApiClient::new(endpoint);
let frames_endpoint = api.frames();
let points = {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.handle().block_on(async move { api.get_points().await })
}
.unwrap();

rustmas_visualizer::run(frames_endpoint, points);
}
16 changes: 16 additions & 0 deletions webapi-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "rustmas-webapi-client"
version = "0.1.0"
edition = "2021"

[dependencies]
animation-api = { path = "../animation-api" }

reqwest = { version = "0.11.12", features = ["json"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
url = "2.5.0"

[features]
default = []
visualizer = []
188 changes: 188 additions & 0 deletions webapi-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use std::{collections::HashMap, fmt};

use animation_api::parameter_schema::ParametersSchema;

use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_json::json;
use url::Url;

#[derive(Debug)]
pub enum GatewayError {
RequestError { reason: String },
InvalidResponse { reason: String },
ApiError { reason: String },
}

impl fmt::Display for GatewayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}

impl std::error::Error for GatewayError {}

type Result<T> = std::result::Result<T, GatewayError>;

#[derive(Deserialize)]
#[serde(untagged)]
enum ApiResponse<T> {
Error { error: String },
Success(T),
}

fn extract_response<T>(res: ApiResponse<T>) -> Result<T> {
match res {
ApiResponse::Success(r) => Ok(r),
ApiResponse::Error { error } => Err(GatewayError::ApiError { reason: error }),
}
}

#[cfg(feature = "visualizer")]
#[derive(Deserialize)]
pub struct GetPointsResponse {
points: Vec<(f32, f32, f32)>,
}

#[derive(Clone, Deserialize)]
pub struct AnimationEntry {
pub id: String,
pub name: String,
}

#[derive(Deserialize)]
struct ListAnimationsResponse {
animations: Vec<AnimationEntry>,
}

#[derive(Debug, Deserialize)]
pub struct Animation {
pub name: String,
pub schema: ParametersSchema,
pub values: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Deserialize)]
struct GetParamsResponse {
animation: Option<Animation>,
}

#[derive(Clone)]
pub struct RustmasApiClient {
endpoint: Url,
client: reqwest::Client,
}

impl RustmasApiClient {
pub fn new(endpoint: Url) -> Self {
Self {
endpoint,
client: reqwest::Client::new(),
}
}

fn url(&self, path: &str) -> String {
self.endpoint.join(path).unwrap().to_string()
}

async fn send_request<T: DeserializeOwned>(request: reqwest::RequestBuilder) -> Result<T> {
request
.send()
.await
.map_err(|e| GatewayError::RequestError {
reason: e.to_string(),
})?
.json::<ApiResponse<T>>()
.await
.map_err(|e| GatewayError::InvalidResponse {
reason: e.to_string(),
})
.and_then(extract_response)
}

async fn post<T: DeserializeOwned>(&self, path: &str, json: &serde_json::Value) -> Result<T> {
Self::send_request::<T>(self.client.post(&self.url(path)).json(json)).await
}

async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
Self::send_request::<T>(self.client.get(&self.url(path))).await
}

#[cfg(feature = "visualizer")]
pub fn frames(&self) -> Url {
let mut endpoint = self.endpoint.clone();
endpoint.set_scheme("ws").unwrap();
endpoint.join("frames").unwrap()
}

#[cfg(feature = "visualizer")]
pub async fn get_points(&self) -> Result<Vec<(f32, f32, f32)>> {
Ok(self.get::<GetPointsResponse>("points").await?.points)
}

pub async fn restart_events(&self) -> Result<()> {
self.post::<()>("restart_events", &json!(())).await?;
Ok(())
}

pub async fn list_animations(&self) -> Result<Vec<AnimationEntry>> {
Ok(self.get::<ListAnimationsResponse>("list").await?.animations)
}

pub async fn discover_animations(&self) -> Result<Vec<AnimationEntry>> {
Ok(self
.get::<ListAnimationsResponse>("discover")
.await?
.animations)
}

pub async fn switch_animation(&self, animation_id: String) -> Result<Option<Animation>> {
Ok(self
.post::<GetParamsResponse>("switch", &json!({ "animation": animation_id}))
.await?
.animation)
}

pub async fn turn_off(&self) -> Result<()> {
self.post::<()>("turn_off", &json!(())).await
}

pub async fn get_params(&self) -> Result<Option<Animation>> {
Ok(self.get::<GetParamsResponse>("params").await?.animation)
}

pub async fn set_params(&self, params: &serde_json::Value) -> Result<()> {
self.post::<()>("params", params).await
}

pub async fn save_params(&self) -> Result<()> {
let _ = self.post::<()>("params/save", &json!(())).await;
Ok(())
}

pub async fn reset_params(&self) -> Result<Option<Animation>> {
Ok(self
.post::<GetParamsResponse>("params/reset", &json!(()))
.await?
.animation)
}

pub async fn reload_animation(&self) -> Result<Option<Animation>> {
Ok(self
.post::<GetParamsResponse>("reload", &json!(()))
.await?
.animation)
}
}

impl Default for RustmasApiClient {
fn default() -> Self {
Self::new(Url::parse("http://localhost/").unwrap())
}
}

impl PartialEq for RustmasApiClient {
fn eq(&self, other: &Self) -> bool {
self.endpoint == other.endpoint
}
}
2 changes: 1 addition & 1 deletion webapi/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async fn post_params(
.set_parameters(params.0)
.await
{
Ok(_) => HttpResponse::Ok().json(json!({})),
Ok(_) => HttpResponse::Ok().json(json!(())),
Err(e) => HttpResponse::InternalServerError().json(json!({"error": e.to_string()})),
}
}
Expand Down
3 changes: 2 additions & 1 deletion webui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
lightfx = { path = "../lightfx" }
animation-api = { path = "../animation-api" }
rustmas-visualizer = { path = "../visualizer", optional = true }
rustmas-webapi-client = { path = "../webapi-client" }

gloo-net = "0.2"
gloo-utils = "0.1"
Expand All @@ -29,4 +30,4 @@ url = "2.5.0"
[features]
default = []
local = []
visualizer = ["rustmas-visualizer"]
visualizer = ["rustmas-visualizer", "rustmas-webapi-client/visualizer"]
Loading

0 comments on commit 9dd7686

Please sign in to comment.