Skip to content

Commit

Permalink
src: server: Add restAPI and websocket services
Browse files Browse the repository at this point in the history
  • Loading branch information
RaulTrombin authored and patrickelectric committed Jul 29, 2024
1 parent 2ccf904 commit a2bfa82
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 5 deletions.
30 changes: 25 additions & 5 deletions src/server/manager.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
use actix_web::{middleware, App, HttpServer};
use paperclip::actix::OpenApiExt;
use crate::device::manager::ManagerActorHandler;

use super::protocols;
use actix_web::{middleware, web::Data, App, HttpServer};
use tracing::info;

pub async fn run(server_address: &str) -> std::io::Result<()> {
use paperclip::actix::{
web::{self, Scope},
OpenApiExt,
};

fn add_v1_paths(scope: Scope) -> Scope {
scope.configure(protocols::v1::rest::register_services)
}

pub async fn run(server_address: &str, handler: ManagerActorHandler) -> std::io::Result<()> {
let server_address = server_address.to_string();
info!("starting HTTP server at http://{server_address}");
info!("ServerManager: Service starting");

let server = HttpServer::new(move || {
let v1 = add_v1_paths(web::scope("/v1"));
let default = add_v1_paths(web::scope(""));

let server = HttpServer::new(|| {
App::new()
.app_data(Data::new(handler.clone()))
.wrap(middleware::Logger::default())
.wrap_api()
.with_json_spec_at("/api/spec")
.with_swagger_ui_at("/docs")
.service(v1)
.service(protocols::v1::rest::server_metadata)
.service(protocols::v1::websocket::websocket)
.service(default)
.build()
});

info!("ServerManager: HTTP server running at http://{server_address}");
server.bind(server_address)?.run().await
}
24 changes: 24 additions & 0 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
pub mod manager;
pub mod protocols;

// The Server module consists of a manager and all available layers that provide access to internal services.
//
// Manager:
// The Manager module requires a DeviceManagerHandler, which will be used to forward all incoming requests.
// This allows the Manager to receive and process requests from RestAPI and WebSocket methods.
// The requests are forwarded to the DeviceManager using the server's AppData, which holds a clone of the DeviceManager's Handler and will provide the responses.
//
// Front-end:
// The frontend provides access to REST API documentation through {address}/docs with a Swagger interface and the API specifications.
//
// RestAPI:
// The REST API will have a default route and versioned routes.
// To keep the application stable through updates, users can use {address}/v{x}/route.
//
// WebSocket:
// WebSocket is provided via the {address}/ws route.
// Users can use the following queries:
// ?filter="some_desired_string_to_use_regex"
// ?device-number="00000000-0000-0000-b9c0-f5752d453eb3" // The UUID provided by the source of the device created
// Otherwise, if they are not defined, the WebSocket channel will receive all available messages.
// All operations made through REST API and WebSocket routes will be broadcast to all clients subscribed to device-number=null (default),
// except for errors, which are forwarded directly to the requester.
1 change: 1 addition & 0 deletions src/server/protocols/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod v1;
40 changes: 40 additions & 0 deletions src/server/protocols/v1/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use actix_web::{http::StatusCode, ResponseError};

use paperclip::actix::api_v2_errors;
use validator::ValidationErrors;

#[allow(dead_code)]
#[api_v2_errors(
code = 400,
description = "Bad Request: The client's request contains invalid or malformed data.",
code = 500,
description = "Internal Server Error: An unexpected server error has occurred."
)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Bad Request: {0}")]
BadRequest(String),
#[error("Internal Server Error: {0}")]
Internal(String),
}

impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
match self {
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

impl From<ValidationErrors> for Error {
fn from(error: ValidationErrors) -> Self {
Self::BadRequest(error.to_string())
}
}

impl From<crate::device::manager::ManagerError> for Error {
fn from(error: crate::device::manager::ManagerError) -> Self {
Self::Internal(serde_json::to_string_pretty(&error).unwrap_or_default())
}
}
61 changes: 61 additions & 0 deletions src/server/protocols/v1/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ping Viewer Next</title>
<script>
function redirectToDocs() {
window.location.href = '/docs/';
}

let socket;

function connectWebSocket() {
socket = new WebSocket('ws://' + window.location.host + '/ws');

socket.onopen = function(event) {
document.getElementById('status').innerText = 'Connected';
};

socket.onmessage = function(event) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.textContent = event.data;
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight; // Auto-scroll to the bottom
};

socket.onclose = function(event) {
document.getElementById('status').innerText = 'Disconnected';
};

socket.onerror = function(event) {
document.getElementById('status').innerText = 'Error';
};
}

function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
if (message && socket.readyState === WebSocket.OPEN) {
socket.send(message);
messageInput.value = '';
}
}

window.onload = function() {
connectWebSocket();
};
</script>
</head>
<body>
<h1>Ping Viewer Next</h1>
<button onclick="redirectToDocs()">Check API specifications</button>
<h2>Websocket Client</h2>
<div id="status">Connecting...</div>
<div id="messages" style="border: 1px solid #ccc; height: 600px; overflow-y: scroll; padding: 5px; white-space: pre-wrap;"></div>
<input type="text" id="messageInput" placeholder="Type your message here" />
<button onclick="sendMessage()">Send</button>
</body>
</html>
3 changes: 3 additions & 0 deletions src/server/protocols/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod errors;
pub mod rest;
pub mod websocket;
92 changes: 92 additions & 0 deletions src/server/protocols/v1/rest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::device::manager::ManagerActorHandler;
use crate::server::protocols::v1::errors::Error;
use actix_web::Responder;
use mime_guess::from_path;
use paperclip::actix::{
api_v2_operation, get, post,
web::{self, HttpResponse, Json},
Apiv2Schema,
};
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(rust_embed::RustEmbed)]
#[folder = "src/server/protocols/v1/frontend"]
struct Asset;

fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => HttpResponse::Ok()
.content_type(from_path(path).first_or_octet_stream().as_ref())
.body(content.data.into_owned()),
None => HttpResponse::NotFound().body("404 Not Found"),
}
}

#[api_v2_operation(skip)]
#[get("/")]
async fn index() -> impl Responder {
handle_embedded_file("index.html")
}

#[api_v2_operation(skip)]
#[get("/{file_path:.*}")]
async fn index_files(file_path: web::Path<String>) -> impl Responder {
handle_embedded_file(&file_path)
}

/// The "register_service" route is used by BlueOS extensions manager
#[api_v2_operation]
#[get("register_service")]
async fn server_metadata() -> Result<Json<ServerMetadata>, Error> {
let package = ServerMetadata::default();
Ok(Json(package))
}

pub fn register_services(cfg: &mut web::ServiceConfig) {
cfg.service(index)
.service(post_request)
.service(index_files);
}

#[api_v2_operation]
#[post("device/request")]
async fn post_request(
manager_handler: web::Data<ManagerActorHandler>,
json: web::Json<crate::device::manager::Request>,
) -> Result<Json<crate::device::manager::Answer>, Error> {
let request = json.into_inner();

let answer = manager_handler.send(request).await?;

// Broadcast the results to webscoket clients.
crate::server::protocols::v1::websocket::send_to_websockets(json!(answer), None);

Ok(Json(answer))
}
#[derive(Debug, Serialize, Deserialize, Apiv2Schema)]
pub struct ServerMetadata {
pub name: &'static str,
pub description: &'static str,
pub icon: &'static str,
pub company: &'static str,
pub version: &'static str,
pub new_page: bool,
pub webpage: &'static str,
pub api: &'static str,
}

impl Default for ServerMetadata {
fn default() -> Self {
Self {
name: "Ping Viewer Next",
description: "A ping protocol extension for expose devices to web.",
icon: "mdi-compass-outline",
company: "BlueRobotics",
version: "0.0.0",
new_page: false,
webpage: "https://github.com/RaulTrombin/navigator-assistant",
api: "/docs",
}
}
}
Loading

0 comments on commit a2bfa82

Please sign in to comment.