-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
src: server: Add restAPI and websocket services
- Loading branch information
1 parent
2ccf904
commit a2bfa82
Showing
8 changed files
with
465 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod v1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub mod errors; | ||
pub mod rest; | ||
pub mod websocket; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
} | ||
} | ||
} |
Oops, something went wrong.