Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 40 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,55 @@ edition = "2021"
url = "^2.5"
log = "^0.4"
regex = "^1.5"
tokio = {version = "^1.32", features = ["full"]}
tokio-util = {version = "^0.6", features = ["codec","net"]}
tokio-stream = {version = "^0.1", features = ["time"]}
tokio = { version = "^1.32", features = ["full"] }
tokio-util = { version = "^0.7", features = ["codec", "net"] }
tokio-stream = { version = "^0.1", features = ["time"] }
futures = "^0.3"
coap-lite = "0.13.1"
async-trait = "0.1.74"

# dependencies for dtls
webrtc-dtls = {version = "0.8.0", optional = true}
webrtc-util = {version = "0.8.0", optional = true}
rustls = {version = "^0.21.1", optional = true}
rustls-pemfile = {version = "2.0.0", optional = true}
rcgen = {version = "^0.11.0", optional = true}
pkcs8 = {version = "0.10.2", optional = true}
sec1 = { version = "0.7.3", features = ["pem", "pkcs8", "std"], optional = true}
rand = "0.8.5"
webrtc-dtls = { version = "0.8.0", optional = true }
webrtc-util = { version = "0.8.0", optional = true }
rustls = { version = "^0.21.1", optional = true }
rustls-pemfile = { version = "2.0.0", optional = true }
rcgen = { version = "^0.11.0", optional = true }
pkcs8 = { version = "0.10.2", optional = true }
sec1 = { version = "0.7.3", features = [
"pem",
"pkcs8",
"std",
], optional = true }
rand = "0.9.0"

# dependencies for extractor
percent-encoding = { version = "2.1", optional = true}
serde = { version = "1.0", optional = true }
serde_html_form = { version = "0.2.7", optional = true }
serde_path_to_error = { version = "0.1.9", optional = true }
serde_json = { version = "1.0", optional = true }

[features]
default = ["dtls"]
dtls = ["dep:webrtc-dtls", "dep:webrtc-util", "dep:rustls", "dep:rustls-pemfile", "dep:rcgen", "dep:pkcs8", "dep:sec1"]
default = ["dtls", "router"]
dtls = [
"dep:webrtc-dtls",
"dep:webrtc-util",
"dep:rustls",
"dep:rustls-pemfile",
"dep:rcgen",
"dep:pkcs8",
"dep:sec1",
]
router = [
"dep:percent-encoding",
"dep:serde",
"dep:serde_html_form",
"dep:serde_path_to_error",
"dep:serde_json",
]


[dev-dependencies]
quickcheck = "0.8.2"
quickcheck = "1.0"
socket2 = "0.5"
tokio-test = "0.4.4"

61 changes: 61 additions & 0 deletions examples/router_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
extern crate coap;

// use coap::client::ObserveMessage;
use coap::UdpCoAPClient;
// use std::io;
use std::io::ErrorKind;

#[tokio::main]
async fn main() {
println!("GET url:");
example_get().await;

println!("POST data to url:");
example_post().await;

println!("GET url again:");
example_get().await;
}

async fn example_get() {
let url = "coap://127.0.0.1:5683/temperature?room=kitchen";
println!("Client request: {}", url);

match UdpCoAPClient::get(url).await {
Ok(response) => {
println!(
"Server reply: {}",
String::from_utf8(response.message.payload).unwrap()
);
}
Err(e) => {
match e.kind() {
ErrorKind::WouldBlock => println!("Request timeout"), // Unix
ErrorKind::TimedOut => println!("Request timeout"), // Windows
_ => println!("Request error: {:?}", e),
}
}
}
}

async fn example_post() {
let url = "coap://127.0.0.1:5683/temperature/kitchen";
let data = b"21.0".to_vec();
println!("Client request: {}", url);

match UdpCoAPClient::post(url, data).await {
Ok(response) => {
println!(
"Server reply: {}",
String::from_utf8(response.message.payload).unwrap()
);
}
Err(e) => {
match e.kind() {
ErrorKind::WouldBlock => println!("Request timeout"), // Unix
ErrorKind::TimedOut => println!("Request timeout"), // Windows
_ => println!("Request error: {:?}", e),
}
}
}
}
67 changes: 67 additions & 0 deletions examples/router_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
extern crate coap;

use coap::{
router::{
extract::{Body, Path, Query, State},
Router,
},
Server,
};
use serde::Deserialize;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;

pub struct RoomState {
rooms: HashMap<String, f64>,
}

pub type RoomMutex = Arc<Mutex<RoomState>>;

#[derive(Debug, Deserialize)]
pub struct QueryArgs {
room: String,
}

async fn get_temperature(
Query(QueryArgs { room }): Query<QueryArgs>,
state: State<RoomMutex>,
) -> String {
println!("get_temperature: {room}");
let state = state.lock().await;

if let Some(temp) = state.rooms.get(&room) {
format!("Temperature in {room}: {temp}")
} else {
format!("Room {} not found", room)
}
}

async fn set_temperature(
Path(room): Path<String>,
Body(temp): Body<f64>,
State(state): State<RoomMutex>,
) -> String {
println!("set_temperature: {:?}", room);
let mut state = state.lock().await;

state.rooms.insert(room, temp);
"OK".to_string()
}

#[tokio::main]
async fn main() {
let addr = "127.0.0.1:5683";

let state = Arc::new(Mutex::new(RoomState {
rooms: HashMap::new(),
}));

let router = Router::new()
.get("/temperature", get_temperature)
.post("/temperature/{room}", set_temperature);

let server = Server::new_udp(addr).unwrap();
println!("Server up on {addr}");

server.serve(router, state).await;
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,6 @@ pub mod client;
pub mod dtls;
mod observer;
pub mod request;
#[cfg(feature = "router")]
pub mod router;
pub mod server;
68 changes: 68 additions & 0 deletions src/router/extract/body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::router::{
extract::FromRequest,
request::Request,
response::{IntoResponse, Response, Status},
};
use serde::de::DeserializeOwned;
use std::ops::{Deref, DerefMut};

#[derive(Debug, Clone, Copy)]
pub enum BodyRejection {
InvalidBody,
DeserializationError,
InvalidUtf8,
}

impl std::fmt::Display for BodyRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BodyRejection::InvalidBody => write!(f, "Invalid body string"),
BodyRejection::DeserializationError => write!(f, "Failed to deserialize JSON body"),
BodyRejection::InvalidUtf8 => write!(f, "Invalid UTF-8 in body"),
}
}
}

impl IntoResponse for BodyRejection {
fn into_response(self) -> Response {
let error_message = self.to_string();
Response::new()
.set_response_type(Status::BadRequest)
.set_payload(error_message.into_bytes())
}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct Body<T>(pub T);

impl<S, T> FromRequest<S> for Body<T>
where
S: Sync,
T: DeserializeOwned,
{
type Rejection = BodyRejection;

async fn from_request(req: &Request, _state: &S) -> Result<Self, Self::Rejection> {
// Convert payload bytes to UTF-8 string
let body_str = String::from_utf8(req.payload()).map_err(|_| BodyRejection::InvalidUtf8)?;

// Deserialize JSON
serde_json::from_str::<T>(&body_str)
.map(Body)
.map_err(|_| BodyRejection::DeserializationError)
}
}

impl<T> Deref for Body<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> DerefMut for Body<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
53 changes: 53 additions & 0 deletions src/router/extract/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::router::{request::Request, response::IntoResponse};
use std::future::Future;

mod body;
mod path;
mod query;
mod state;
mod tuple;

pub trait FromRequest<S>: Sized {
type Rejection: IntoResponse;

fn from_request(
req: &Request,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}

pub trait FromRef<T> {
fn from_ref(input: &T) -> Self;
}

impl<T: Clone> FromRef<T> for T {
fn from_ref(input: &T) -> Self {
input.clone()
}
}

pub trait OptionalFromRequest<S>: Sized {
type Rejection: IntoResponse;

fn from_request(
req: &Request,
state: &S,
) -> impl Future<Output = Result<Option<Self>, Self::Rejection>> + Send;
}

impl<S, T> FromRequest<S> for Option<T>
where
T: OptionalFromRequest<S>,
S: Send + Sync,
{
type Rejection = T::Rejection;

async fn from_request(req: &Request, state: &S) -> Result<Option<T>, Self::Rejection> {
T::from_request(req, state).await
}
}

pub use body::Body;
pub use path::Path;
pub use query::Query;
pub use state::State;
Loading
Loading