pub mod http;
pub mod nats;
pub mod serve;
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tatara_lisp_eval::ffi::Arity;
use tatara_lisp_eval::{EvalError, Interpreter, Map, MapKey, Value};
use crate::sys::{arg_int, arg_str};
const DEFAULT_CONNECT_MS: i64 = 5_000;
pub fn split_host_port(s: &str, default_port: u16) -> Result<(String, u16), String> {
let (host, port) = if let Some(rest) = s.strip_prefix('[') {
let (h, after) = rest.split_once(']').ok_or("unclosed [ in host")?;
(h, after.strip_prefix(':'))
} else {
match s.rsplit_once(':') {
Some((h, p)) => (h, Some(p)),
None => (s, None),
}
};
if host.is_empty() {
return Err("no host".into());
}
let port = match port {
Some(p) => p.parse().map_err(|_| format!("bad port {p:?}"))?,
None => default_port,
};
Ok((host.to_string(), port))
}
pub fn dial(host: &str, port: u16, timeout: Duration) -> Result<TcpStream, String> {
let addrs: Vec<_> = (host, port)
.to_socket_addrs()
.map_err(|e| format!("{host}:{port}: {e}"))?
.collect();
let mut last = format!("{host}:{port}: no address");
for addr in addrs {
match TcpStream::connect_timeout(&addr, timeout) {
Ok(s) => {
let _ = s.set_nodelay(true);
return Ok(s);
}
Err(e) => last = format!("{host}:{port}: {e}"),
}
}
Err(last)
}
struct NatsConnection(Mutex<nats::Client>);
struct NatsSubscription {
conn: Arc<NatsConnection>,
sid: u64,
}
type Span = tatara_lisp::Span;
fn connection(
v: &Value,
fname: &'static str,
span: Span,
) -> Result<Arc<NatsConnection>, EvalError> {
if let Value::Foreign(any) = v {
if let Ok(conn) = Arc::downcast::<NatsConnection>(any.clone()) {
return Ok(conn);
}
}
Err(EvalError::native_fn(
fname,
format!(
"expected a NATS connection from nats_connect, got {}",
v.type_name()
),
span,
))
}
fn subscription(
v: &Value,
fname: &'static str,
span: Span,
) -> Result<Arc<NatsSubscription>, EvalError> {
if let Value::Foreign(any) = v {
if let Ok(sub) = Arc::downcast::<NatsSubscription>(any.clone()) {
return Ok(sub);
}
}
Err(EvalError::native_fn(
fname,
format!(
"expected a subscription from nats_subscribe, got {}",
v.type_name()
),
span,
))
}
fn with_client<T>(
conn: &NatsConnection,
fname: &'static str,
span: Span,
f: impl FnOnce(&mut nats::Client) -> Result<T, nats::NatsError>,
) -> Result<T, EvalError> {
let mut client = conn
.0
.lock()
.map_err(|_| EvalError::native_fn(fname, "the connection's lock is poisoned", span))?;
f(&mut client).map_err(|e| EvalError::native_fn(fname, e.to_string(), span))
}
fn arg_ms(
v: &Value,
fname: &'static str,
span: Span,
zero_ok: bool,
) -> Result<Duration, EvalError> {
let ms = arg_int(v, fname, span)?;
if ms < 0 || (ms == 0 && !zero_ok) {
return Err(EvalError::native_fn(
fname,
format!(
"timeout must be {} milliseconds, got {ms}",
if zero_ok { ">= 0" } else { "> 0" }
),
span,
));
}
Ok(Duration::from_millis(ms as u64))
}
fn text(bytes: &[u8]) -> Value {
Value::Str(Arc::from(String::from_utf8_lossy(bytes).as_ref()))
}
fn record(fields: Vec<(&str, Value)>) -> Value {
let mut m = Map::new();
for (k, v) in fields {
m.insert(MapKey::Keyword(Arc::from(k)), v);
}
Value::Map(Arc::new(m))
}
fn header_text(v: &Value, fname: &'static str, span: Span) -> Result<String, EvalError> {
match v {
Value::Str(s) | Value::Keyword(s) | Value::Symbol(s) => Ok(s.to_string()),
Value::Int(n) => Ok(n.to_string()),
other => Err(EvalError::native_fn(
fname,
format!(
"a header name or value must be a string, got {}",
other.type_name()
),
span,
)),
}
}
fn headers_arg(
v: &Value,
fname: &'static str,
span: Span,
) -> Result<Vec<(String, String)>, EvalError> {
match v {
Value::Nil => Ok(Vec::new()),
Value::Map(m) => {
let mut out = Vec::with_capacity(m.len());
for (k, val) in m.iter() {
let name = match k {
MapKey::Str(s) | MapKey::Keyword(s) | MapKey::Symbol(s) => s.to_string(),
other => {
return Err(EvalError::native_fn(
fname,
format!("a header name must be a string, got {other:?}"),
span,
));
}
};
out.push((name, header_text(val, fname, span)?));
}
out.sort();
Ok(out)
}
Value::List(items) => items
.iter()
.map(|it| match it {
Value::List(kv) if kv.len() == 2 => Ok((
header_text(&kv[0], fname, span)?,
header_text(&kv[1], fname, span)?,
)),
_ => Err(EvalError::native_fn(
fname,
"each header must be a [name, value] pair",
span,
)),
})
.collect(),
other => Err(EvalError::native_fn(
fname,
format!(
"headers must be nil, a map or [[name, value], …], got {}",
other.type_name()
),
span,
)),
}
}
pub fn install_net<H: 'static>(interp: &mut Interpreter<H>) {
install_nats(interp);
install_http(interp);
install_serve(interp);
}
fn install_nats<H: 'static>(interp: &mut Interpreter<H>) {
interp.register_fn(
"nats_connect",
Arity::Range(1, 2),
|args: &[Value], _h: &mut H, span| {
let url = arg_str(&args[0], "nats_connect", span)?;
let timeout = match args.get(1) {
Some(v) => arg_ms(v, "nats_connect", span, false)?,
None => Duration::from_millis(DEFAULT_CONNECT_MS as u64),
};
let client = nats::Client::connect(&url, timeout)
.map_err(|e| EvalError::native_fn("nats_connect", format!("{url}: {e}"), span))?;
Ok(Value::Foreign(Arc::new(NatsConnection(Mutex::new(client)))))
},
);
interp.register_fn(
"nats_publish",
Arity::Exact(3),
|args: &[Value], _h: &mut H, span| {
let conn = connection(&args[0], "nats_publish", span)?;
let subject = arg_str(&args[1], "nats_publish", span)?;
let payload = arg_str(&args[2], "nats_publish", span)?;
with_client(&conn, "nats_publish", span, |c| {
c.publish(&subject, None, payload.as_bytes())
})?;
Ok(Value::Nil)
},
);
interp.register_fn(
"nats_request",
Arity::Exact(4),
|args: &[Value], _h: &mut H, span| {
let conn = connection(&args[0], "nats_request", span)?;
let subject = arg_str(&args[1], "nats_request", span)?;
let payload = arg_str(&args[2], "nats_request", span)?;
let timeout = arg_ms(&args[3], "nats_request", span, false)?;
let reply = with_client(&conn, "nats_request", span, |c| {
c.request(&subject, payload.as_bytes(), timeout)
})?;
Ok(text(&reply.payload))
},
);
interp.register_fn(
"nats_subscribe",
Arity::Range(2, 3),
|args: &[Value], _h: &mut H, span| {
let conn = connection(&args[0], "nats_subscribe", span)?;
let subject = arg_str(&args[1], "nats_subscribe", span)?;
let queue = match args.get(2) {
Some(v) => Some(arg_str(v, "nats_subscribe", span)?),
None => None,
};
let sid = with_client(&conn, "nats_subscribe", span, |c| {
c.subscribe(&subject, queue.as_deref())
})?;
Ok(Value::Foreign(Arc::new(NatsSubscription { conn, sid })))
},
);
interp.register_fn(
"nats_next_message",
Arity::Exact(2),
|args: &[Value], _h: &mut H, span| {
let sub = subscription(&args[0], "nats_next_message", span)?;
let timeout = arg_ms(&args[1], "nats_next_message", span, true)?;
let next = with_client(&sub.conn, "nats_next_message", span, |c| {
c.next_message(sub.sid, timeout)
})?;
Ok(next.map_or(Value::Nil, |m| {
record(vec![
("subject", Value::Str(Arc::from(m.subject))),
("payload", text(&m.payload)),
(
"reply",
m.reply.map_or(Value::Nil, |r| Value::Str(Arc::from(r))),
),
])
}))
},
);
interp.register_fn(
"nats_unsubscribe",
Arity::Exact(1),
|args: &[Value], _h: &mut H, span| {
let sub = subscription(&args[0], "nats_unsubscribe", span)?;
with_client(&sub.conn, "nats_unsubscribe", span, |c| {
c.unsubscribe(sub.sid)
})?;
Ok(Value::Nil)
},
);
interp.register_fn(
"nats_close",
Arity::Exact(1),
|args: &[Value], _h: &mut H, span| {
let conn = connection(&args[0], "nats_close", span)?;
with_client(&conn, "nats_close", span, |c| {
c.close();
Ok(())
})?;
Ok(Value::Nil)
},
);
}
fn install_http<H: 'static>(interp: &mut Interpreter<H>) {
interp.register_fn(
"http_request",
Arity::Exact(5),
|args: &[Value], _h: &mut H, span| {
let method = arg_str(&args[0], "http_request", span)?;
let url = arg_str(&args[1], "http_request", span)?;
let headers = headers_arg(&args[2], "http_request", span)?;
let body = match &args[3] {
Value::Nil => None,
other => Some(arg_str(other, "http_request", span)?),
};
let timeout = arg_ms(&args[4], "http_request", span, false)?;
let resp = http::request(
&method,
&url,
&headers,
body.as_deref().map(str::as_bytes),
timeout,
)
.map_err(|e| EvalError::native_fn("http_request", e.to_string(), span))?;
Ok(record(vec![
("status", Value::Int(i64::from(resp.status))),
(
"headers",
Value::list(
resp.headers
.into_iter()
.map(|(k, v)| {
Value::list(vec![
Value::Str(Arc::from(k)),
Value::Str(Arc::from(v)),
])
})
.collect::<Vec<_>>(),
),
),
("body", text(&resp.body)),
]))
},
);
}
struct HttpListener(serve::Listener);
struct HttpExchange(Mutex<Option<TcpStream>>);
fn listener(v: &Value, fname: &'static str, span: Span) -> Result<Arc<HttpListener>, EvalError> {
if let Value::Foreign(any) = v {
if let Ok(l) = Arc::downcast::<HttpListener>(any.clone()) {
return Ok(l);
}
}
Err(EvalError::native_fn(
fname,
format!(
"expected a listener from http_listen, got {}",
v.type_name()
),
span,
))
}
fn exchange(v: &Value, fname: &'static str, span: Span) -> Result<Arc<HttpExchange>, EvalError> {
let inner = match v {
Value::Map(m) => m.get(&MapKey::Keyword(Arc::from("exchange"))).cloned(),
other => Some(other.clone()),
};
if let Some(Value::Foreign(any)) = inner {
if let Ok(x) = Arc::downcast::<HttpExchange>(any) {
return Ok(x);
}
}
Err(EvalError::native_fn(
fname,
format!(
"expected a request from http_next_request, got {}",
v.type_name()
),
span,
))
}
fn pairs(headers: Vec<(String, String)>) -> Value {
Value::list(
headers
.into_iter()
.map(|(k, v)| Value::list(vec![Value::Str(Arc::from(k)), Value::Str(Arc::from(v))]))
.collect::<Vec<_>>(),
)
}
fn install_serve<H: 'static>(interp: &mut Interpreter<H>) {
interp.register_fn(
"http_listen",
Arity::Exact(1),
|args: &[Value], _h: &mut H, span| {
let addr = arg_str(&args[0], "http_listen", span)?;
let l = serve::Listener::bind(&addr)
.map_err(|e| EvalError::native_fn("http_listen", e.to_string(), span))?;
Ok(Value::Foreign(Arc::new(HttpListener(l))))
},
);
interp.register_fn(
"http_listener_port",
Arity::Exact(1),
|args: &[Value], _h: &mut H, span| {
let l = listener(&args[0], "http_listener_port", span)?;
let addr =
l.0.local_addr()
.map_err(|e| EvalError::native_fn("http_listener_port", e.to_string(), span))?;
Ok(Value::Int(i64::from(addr.port())))
},
);
interp.register_fn(
"http_next_request",
Arity::Exact(2),
|args: &[Value], _h: &mut H, span| {
let l = listener(&args[0], "http_next_request", span)?;
let timeout = arg_ms(&args[1], "http_next_request", span, true)?;
let next =
l.0.next(timeout)
.map_err(|e| EvalError::native_fn("http_next_request", e.to_string(), span))?;
Ok(next.map_or(Value::Nil, |(req, stream)| {
record(vec![
("method", Value::Str(Arc::from(req.method))),
("path", Value::Str(Arc::from(req.path))),
("query", Value::Str(Arc::from(req.query))),
("headers", pairs(req.headers)),
("body", text(&req.body)),
(
"exchange",
Value::Foreign(Arc::new(HttpExchange(Mutex::new(Some(stream))))),
),
])
}))
},
);
interp.register_fn(
"http_respond",
Arity::Exact(4),
|args: &[Value], _h: &mut H, span| {
let x = exchange(&args[0], "http_respond", span)?;
let status = arg_int(&args[1], "http_respond", span)?;
let status = u16::try_from(status).map_err(|_| {
EvalError::native_fn("http_respond", format!("{status} is not a status"), span)
})?;
let headers = headers_arg(&args[2], "http_respond", span)?;
let body = match &args[3] {
Value::Nil => String::new(),
other => arg_str(other, "http_respond", span)?.to_string(),
};
let stream = x
.0
.lock()
.map_err(|_| {
EvalError::native_fn("http_respond", "the exchange's lock is poisoned", span)
})?
.take()
.ok_or_else(|| {
EvalError::native_fn("http_respond", "this request was already answered", span)
})?;
serve::respond(&stream, status, &headers, body.as_bytes())
.map_err(|e| EvalError::native_fn("http_respond", e.to_string(), span))?;
Ok(Value::Nil)
},
);
}