use std::rc::Rc;
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyBytes, PyDict, PyFloat, PyInt, PyList, PyNone, PyString};
use wingfoil::adapters::web::{CodecKind, WebServer, WebServerBuilder, web_pub, web_sub};
use wingfoil::{Burst, Node, Stream, StreamOperators};
use crate::py_element::PyElement;
use crate::py_stream::PyStream;
use crate::types::{DICT_INSERT_INFALLIBLE, INTO_PY_INFALLIBLE, LIST_NEW_INFALLIBLE};
#[pyclass(unsendable, name = "WebServer")]
pub struct PyWebServer {
inner: WebServer,
}
impl PyWebServer {
pub(crate) fn inner_ref(&self) -> &WebServer {
&self.inner
}
}
#[pymethods]
impl PyWebServer {
#[new]
#[pyo3(signature = (addr, codec=None, static_dir=None, historical=false))]
fn new(
addr: String,
codec: Option<String>,
static_dir: Option<String>,
historical: bool,
) -> PyResult<Self> {
let mut builder: WebServerBuilder = WebServer::bind(addr);
if let Some(c) = codec {
let kind = match c.as_str() {
"bincode" => CodecKind::Bincode,
"json" => CodecKind::Json,
other => {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"unknown codec '{other}' (expected 'bincode' or 'json')"
)));
}
};
builder = builder.codec(kind);
}
if let Some(dir) = static_dir {
builder = builder.serve_static(dir);
}
let inner = if historical {
builder.start_historical()
} else {
builder.start()
}
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
Ok(Self { inner })
}
fn port(&self) -> u16 {
self.inner.port()
}
fn codec_name(&self) -> &'static str {
match self.inner.codec() {
CodecKind::Bincode => "bincode",
CodecKind::Json => "json",
}
}
fn sub(&self, topic: String) -> PyStream {
py_web_sub(&self.inner, topic)
}
}
pub fn py_web_pub_inner(
stream: &Rc<dyn Stream<PyElement>>,
server: &WebServer,
topic: String,
) -> Rc<dyn Node> {
let value_stream: Rc<dyn Stream<serde_json::Value>> = stream.map(|elem| {
Python::attach(|py| py_to_serde(elem.as_ref().bind(py)).unwrap_or(serde_json::Value::Null))
});
web_pub(server, topic, &value_stream)
}
pub fn py_web_sub(server: &WebServer, topic: String) -> PyStream {
let burst_stream: Rc<dyn Stream<Burst<serde_json::Value>>> = web_sub(server, topic);
let py_stream = burst_stream.map(|burst| {
Python::attach(|py| {
let items: Vec<Py<PyAny>> = burst
.into_iter()
.map(|v| serde_to_py(py, &v).unbind())
.collect();
PyElement::new(
PyList::new(py, items)
.expect(LIST_NEW_INFALLIBLE)
.into_any()
.unbind(),
)
})
});
PyStream(py_stream)
}
fn py_to_serde(obj: &Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
if obj.is_instance_of::<PyNone>() {
return Ok(serde_json::Value::Null);
}
if obj.is_instance_of::<PyBool>() {
return Ok(serde_json::Value::Bool(obj.extract::<bool>()?));
}
if obj.is_instance_of::<PyInt>() {
if let Ok(v) = obj.extract::<i64>() {
return Ok(serde_json::Value::Number(v.into()));
}
if let Ok(v) = obj.extract::<u64>() {
return Ok(serde_json::Value::Number(v.into()));
}
if let Ok(v) = obj.extract::<f64>() {
return serde_json::Number::from_f64(v)
.map(serde_json::Value::Number)
.ok_or_else(|| {
pyo3::exceptions::PyValueError::new_err("web_pub: non-finite float")
});
}
}
if obj.is_instance_of::<PyFloat>() {
let v = obj.extract::<f64>()?;
return serde_json::Number::from_f64(v)
.map(serde_json::Value::Number)
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("web_pub: non-finite float"));
}
if obj.is_instance_of::<PyString>() {
return Ok(serde_json::Value::String(obj.extract::<String>()?));
}
if let Ok(bytes) = obj.cast::<PyBytes>() {
let data = bytes.as_bytes();
let arr: Vec<serde_json::Value> = data
.iter()
.map(|b| serde_json::Value::Number((*b as u64).into()))
.collect();
return Ok(serde_json::Value::Array(arr));
}
if let Ok(list) = obj.cast::<PyList>() {
let mut out = Vec::with_capacity(list.len());
for item in list.iter() {
out.push(py_to_serde(&item)?);
}
return Ok(serde_json::Value::Array(out));
}
if let Ok(dict) = obj.cast::<PyDict>() {
let mut map = serde_json::Map::new();
for (k, v) in dict.iter() {
let key = k.extract::<String>().map_err(|_| {
pyo3::exceptions::PyTypeError::new_err(
"web_pub: dict keys must be strings for JSON-compatible publish",
)
})?;
map.insert(key, py_to_serde(&v)?);
}
return Ok(serde_json::Value::Object(map));
}
Err(pyo3::exceptions::PyTypeError::new_err(format!(
"web_pub: unsupported value type: {}",
obj.get_type().name()?
)))
}
fn serde_to_py<'py>(py: Python<'py>, value: &serde_json::Value) -> Bound<'py, PyAny> {
use serde_json::Value as V;
match value {
V::Null => py.None().into_bound(py),
V::Bool(b) => PyBool::new(py, *b).to_owned().into_any(),
V::Number(n) => {
if let Some(i) = n.as_i64() {
i.into_pyobject(py).expect(INTO_PY_INFALLIBLE).into_any()
} else if let Some(u) = n.as_u64() {
u.into_pyobject(py).expect(INTO_PY_INFALLIBLE).into_any()
} else if let Some(f) = n.as_f64() {
f.into_pyobject(py).expect(INTO_PY_INFALLIBLE).into_any()
} else {
py.None().into_bound(py)
}
}
V::String(s) => s
.as_str()
.into_pyobject(py)
.expect(INTO_PY_INFALLIBLE)
.into_any(),
V::Array(arr) => {
let items: Vec<Bound<'py, PyAny>> = arr.iter().map(|v| serde_to_py(py, v)).collect();
PyList::new(py, items)
.expect(LIST_NEW_INFALLIBLE)
.into_any()
}
V::Object(obj) => {
let d = PyDict::new(py);
for (k, v) in obj.iter() {
d.set_item(k, serde_to_py(py, v))
.expect(DICT_INSERT_INFALLIBLE);
}
d.into_any()
}
}
}