pub mod extract;
pub mod handler;
pub mod macros;
pub mod method_routing;
pub mod request;
pub mod response;
pub mod route;
pub mod util;
pub use coap_lite::RequestType as Method;
use handler::{BoxedHandler, Handler, HandlerWrapper};
use method_routing::MethodRouter;
pub use method_routing::{delete, fallback, get, post, put};
pub use request::Request;
use response::IntoResponse;
use route::{Route, RouteError};
#[derive(Default)]
pub struct Router<S = ()> {
routes: Vec<(Route, MethodRouter<S>)>,
fallback: Option<BoxedHandler<S>>,
state: S,
}
impl<S: Clone + Default + Send + Sync + 'static> Router<S> {
pub fn new() -> Self {
Self::from_state(Default::default())
}
}
impl<S: Clone + Send + Sync + 'static> Router<S> {
pub fn from_state(state: S) -> Self {
Self {
routes: Vec::new(),
fallback: None,
state,
}
}
pub fn with_state(self, state: S) -> Self {
Self { state, ..self }
}
pub fn route(mut self, path: impl ToString, method_router: MethodRouter<S>) -> Self {
let route = Route::new(path);
self.routes.push((route, method_router));
self
}
pub fn fallback<H, T>(mut self, handler: H) -> Self
where
T: Send + Sync + 'static,
H: Handler<T, S>,
{
let handler = Box::new(HandlerWrapper::new(handler));
self.fallback = Some(handler);
self
}
pub(crate) async fn handle(&self, mut req: Request) -> Request {
for (route, handler) in &self.routes {
if let Ok(path) = route.match_path(&req.path()) {
req.path = path;
match handler.try_handle(req, self.state.clone()).await {
Ok(req) => return req,
Err(req) => {
return self.handle_fallback(req).await;
}
}
}
}
self.handle_fallback(req).await
}
async fn handle_fallback(&self, mut req: Request) -> Request {
match self.fallback {
Some(ref fallback) => fallback.call(req, self.state.clone()).await,
None => {
RouteError::NotFound.into_response().fill_response(&mut req);
req
}
}
}
}
#[cfg(test)]
pub(crate) mod test_utils {
use crate::{
router::{
extract::{Json, Path, Query, State},
method_routing::{delete, fallback, get, post, put},
response::{IntoResponse, Response, StatusCode},
Router,
},
Server,
};
use serde::Deserialize;
use std::{
collections::HashMap,
fmt::{Display, Formatter, Result},
sync::{Arc, Mutex},
};
#[derive(Debug, Clone, Default)]
pub(crate) struct AppState(Arc<Mutex<HashMap<String, String>>>);
impl AppState {
pub(crate) fn new() -> Self {
AppState(Arc::new(Mutex::new(HashMap::new())))
}
pub(crate) fn insert(&self, key: String, value: String) {
let mut map = self.0.lock().unwrap();
map.insert(key, value);
}
pub(crate) fn clear_state(&self) {
let mut map = self.0.lock().unwrap();
map.clear();
}
pub(crate) fn remove(&self, key: &str) -> Option<String> {
let mut map = self.0.lock().unwrap();
map.remove(key)
}
pub(crate) fn get_property(&self, key: &str) -> Option<String> {
let map = self.0.lock().unwrap();
map.get(key).cloned()
}
pub(crate) fn get_state(&self) -> HashMap<String, String> {
self.0.lock().unwrap().clone()
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct QueryArgs {
pub key: String,
pub value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Error {
PropertyNotFound,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Error::PropertyNotFound => write!(f, "Property not found"),
}
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let payload = self.to_string().into_bytes();
Response::new()
.set_status_code(StatusCode::NotFound)
.set_payload(payload)
}
}
impl std::error::Error for Error {}
pub(crate) async fn get_state(state: State<AppState>) -> impl IntoResponse {
let state = state.get_state();
Json(state).into_response()
}
pub(crate) async fn delete_state(state: State<AppState>) -> impl IntoResponse {
state.clear_state();
StatusCode::Valid.into_response()
}
pub(crate) async fn get_property(
query: Query<QueryArgs>,
state: State<AppState>,
) -> impl IntoResponse {
match state.get_property(&query.key) {
Some(value) => (StatusCode::Valid, Json(value)).into_response(),
None => Json(Error::PropertyNotFound).into_response(),
}
}
pub(crate) async fn post_property_query(
query: Query<QueryArgs>,
state: State<AppState>,
) -> impl IntoResponse {
if let Some(value) = query.value.as_ref() {
if state.get_property(&query.key).is_some() {
(StatusCode::BadRequest, "Property already exists").into_response()
} else {
state.insert(query.key.clone(), value.clone());
StatusCode::Valid.into_response()
}
} else {
(StatusCode::BadRequest, "Missing value in query").into_response()
}
}
pub(crate) async fn post_property(
key: Path<String>,
state: State<AppState>,
Json(value): Json<String>,
) -> impl IntoResponse {
if state.get_property(&key).is_some() {
(StatusCode::BadRequest, "Property already exists").into_response()
} else {
state.insert(key.clone(), value);
StatusCode::Valid.into_response()
}
}
pub(crate) async fn put_property(
key: Path<String>,
state: State<AppState>,
Json(value): Json<String>,
) -> impl IntoResponse {
if state.get_property(&key).is_some() {
state.insert(key.clone(), value);
StatusCode::Valid.into_response()
} else {
(StatusCode::NotFound, "Property not found").into_response()
}
}
pub(crate) async fn delete_property(
Path(key): Path<String>,
state: State<AppState>,
) -> impl IntoResponse {
match state.remove(&key) {
Some(_) => StatusCode::Valid.into_response(),
None => Json(Error::PropertyNotFound).into_response(),
}
}
pub(crate) async fn fallback_handler() -> impl IntoResponse {
(StatusCode::BadRequest, "Fallback response").into_response()
}
pub(crate) async fn route_fallback_handler() -> impl IntoResponse {
(StatusCode::BadRequest, "Route fallback response").into_response()
}
pub(crate) fn build_router() -> Router<AppState> {
Router::new()
.with_state(AppState::new())
.route(
"/state",
delete(delete_state)
.get(get_state)
.fallback(route_fallback_handler),
)
.route(
"/state/property/{key}",
put(put_property)
.post(post_property)
.delete(delete_property),
)
.route("/property", get(get_property).post(post_property_query))
.route(
"/property/{key}",
post(post_property)
.put(put_property)
.delete(delete_property),
)
.fallback(fallback_handler)
.route("/fallback", fallback(route_fallback_handler))
}
pub(crate) async fn run_router<S: ToString>(addr: S) {
let router = build_router();
let server = Server::new_udp(addr.to_string()).unwrap();
server.serve(router).await.unwrap();
}
pub(crate) fn encode_payload(payload: &str) -> Vec<u8> {
if serde_json::from_str::<serde_json::Value>(payload).is_ok() {
payload.as_bytes().to_vec()
} else {
serde_json::to_vec(payload).expect("string payload should always serialize to JSON")
}
}
}
#[cfg(test)]
mod tests {
use super::response::StatusCode;
use super::test_utils::*;
use crate::client::UdpCoAPClient as Client;
use coap_lite::RequestType as Method;
use tokio::time::{sleep, Duration};
#[tokio::test]
async fn test_router() {
let addr = "127.0.0.1:5684";
let server_handle = tokio::spawn(async move {
run_router(addr).await;
});
sleep(Duration::from_millis(100)).await;
let response = Client::get(&format!("coap://{}/wrong_path", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Fallback response");
let response = Client::post(&format!("coap://{}/state", addr), b"{}".to_vec()).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Route fallback response");
let response = Client::get(&format!("coap://{}/fallback", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Route fallback response");
let response =
Client::request(&format!("coap://{}/property", addr), Method::Fetch, None).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Fallback response");
let response = Client::get(&format!("coap://{}/state", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::Content);
assert_eq!(payload, "{}");
let response = Client::get(&format!("coap://{}/property?wrong=test_key", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Invalid query string");
let response = Client::get(&format!("coap://{}/property?key=test_key", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::NotFound);
assert_eq!(payload, "Property not found");
let response = Client::put(
&format!("coap://{}/property/test_key", addr),
encode_payload("test_value"),
)
.await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::NotFound);
assert_eq!(payload, "Property not found");
let response = Client::post(
&format!("coap://{}/property/test_key", addr),
encode_payload("test_value"),
)
.await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
assert_eq!(status, StatusCode::Valid);
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(payload, "");
let response = Client::get(&format!("coap://{}/property?key=test_key", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::Valid);
assert_eq!(payload, r#""test_value""#);
let response = Client::post(
&format!("coap://{}/property/test_key", addr),
encode_payload("test_value"),
)
.await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Property already exists");
let response = Client::put(
&format!("coap://{}/property/test_key", addr),
encode_payload("updated_value"),
)
.await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
assert_eq!(status, StatusCode::Valid);
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(payload, "");
let response = Client::delete(&format!("coap://{}/property/test_key", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
assert_eq!(status, StatusCode::Valid);
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(payload, "");
let response = Client::post(
&format!("coap://{}/property?key=query_key", addr),
Vec::new(),
)
.await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(status, StatusCode::BadRequest);
assert_eq!(payload, "Missing value in query");
let response = Client::post(
&format!("coap://{}/property?key=query_key&value=query_value", addr),
Vec::new(),
)
.await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
assert_eq!(status, StatusCode::Valid);
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(payload, "");
let response = Client::delete(&format!("coap://{}/state", addr)).await;
assert!(response.is_ok());
let response = response.unwrap();
let status = *response.get_status();
assert_eq!(status, StatusCode::Valid);
let payload = String::from_utf8(response.message.payload).unwrap();
assert_eq!(payload, "");
server_handle.abort();
let _ = server_handle.await;
}
}