pub mod models;
use axum::{
extract::{Path as AxumPath, Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
Json, Router,
};
use models::{HostInfo, OscRootNode};
use serde_json::json;
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
sync::Arc,
};
use tokio::sync::oneshot::Sender as OneshotSender;
use tokio::{
net::{TcpListener, ToSocketAddrs},
sync::RwLock,
};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to bind to address: {0}")]
BindError(#[from] std::io::Error),
#[error("Failed to parse address: {0}")]
ParseError(#[from] std::net::AddrParseError),
}
pub struct OscQuery {
shutdown_tx: Option<OneshotSender<()>>,
state: Arc<OscQueryState>,
}
pub struct OscQueryState {
host_info: RwLock<HostInfo>,
root: OscRootNode,
}
impl OscQuery {
pub fn new(host_info: HostInfo, root: OscRootNode) -> Self {
let state = Arc::new(OscQueryState {
host_info: RwLock::new(host_info),
root,
});
Self {
shutdown_tx: None, state,
}
}
pub async fn serve<T>(&mut self, addr: T) -> Result<SocketAddr, Error>
where
T: ToSocketAddrs,
{
let shared_state = self.state.clone();
let listener = TcpListener::bind(addr).await?;
let local_addr = listener.local_addr()?;
let app = Router::new()
.route("/", get(handle_root)) .route("/{*path}", get(handle_path)) .with_state(shared_state);
let (tx, rx) = tokio::sync::oneshot::channel();
self.shutdown_tx = Some(tx);
tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
rx.await.ok(); })
.await
.unwrap_or_else(|e| log::error!("OSCQuery server error: {}", e));
});
Ok(local_addr) }
pub fn shutdown(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
}
pub async fn set_ip(&self, ip: IpAddr) {
let mut host_info_guard = self.state.host_info.write().await;
host_info_guard.osc_ip = ip;
}
pub async fn set_port(&self, port: u16) {
let mut host_info_guard = self.state.host_info.write().await;
host_info_guard.osc_port = port;
}
}
async fn handle_root(
params: Query<HashMap<String, String>>, state: State<Arc<OscQueryState>>, ) -> impl IntoResponse {
handle_path(AxumPath(String::new()), params, state).await
}
async fn handle_path(
AxumPath(path_str): AxumPath<String>, Query(params): Query<HashMap<String, String>>, State(state): State<Arc<OscQueryState>>, ) -> impl IntoResponse {
let full_path = format!("/{}", path_str);
let Some(node) = state.root.get_node(&full_path) else {
return (StatusCode::NOT_FOUND, "Node not found").into_response();
};
if params.is_empty() {
Json(json!(node)).into_response()
} else if params.contains_key("HOST_INFO") {
let host_info_guard = state.host_info.read().await;
Json(json!(*host_info_guard)).into_response()
} else if params.len() == 1 {
let (attr_key, _attr_val) = params.iter().next().unwrap(); let attr_key_uppercase = attr_key.to_uppercase();
let node_json = json!(node);
if let Some(value) = node_json.get(&attr_key_uppercase) {
Json(json!({ attr_key_uppercase: value })).into_response()
} else {
(
StatusCode::NOT_FOUND,
format!(
"Attribute {} not found on node {}",
attr_key_uppercase, full_path
),
)
.into_response()
}
} else {
(
StatusCode::BAD_REQUEST,
"Invalid or too many query parameters. Use a single attribute query or HOST_INFO.",
)
.into_response()
}
}