use std::fmt;
use std::io::{BufRead, Write};
use std::num::NonZeroU64;
use std::str::FromStr;
use std::sync::mpsc::{self, RecvTimeoutError};
use std::time::{Duration, Instant};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::ask::{Address, Answer, AskOptions, Correlation, Pending};
use crate::config::{Bus, BusError};
use crate::queue::{Asker, Pushed};
use crate::transport::QueueName;
pub const DEFAULT_REPLY_WINDOW: Duration = Duration::from_secs(30);
const CODEC_NAME_LIMIT: usize = 64;
const ENV_NAME_LIMIT: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct CodecName(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct EnvName(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{text:?} is not {what}: {why}")]
pub struct NameRefused {
pub text: String,
pub what: &'static str,
pub why: &'static str,
}
impl CodecName {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl EnvName {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for CodecName {
type Err = NameRefused;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let refuse = |why| NameRefused {
text: text.to_owned(),
what: "a codec name",
why,
};
let Some(first) = text.bytes().next() else {
return Err(refuse("it is empty"));
};
if text.len() > CODEC_NAME_LIMIT {
return Err(refuse("it is longer than a codec name can be"));
}
if !first.is_ascii_lowercase() {
return Err(refuse("it does not start with a lowercase letter"));
}
if !text
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
{
return Err(refuse(
"it carries something other than lowercase letters, digits and `-`",
));
}
Ok(Self(text.to_owned()))
}
}
impl FromStr for EnvName {
type Err = NameRefused;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let refuse = |why| NameRefused {
text: text.to_owned(),
what: "an environment variable name",
why,
};
let Some(first) = text.bytes().next() else {
return Err(refuse("it is empty"));
};
if text.len() > ENV_NAME_LIMIT {
return Err(refuse("it is longer than a variable name can be"));
}
if first.is_ascii_digit() {
return Err(refuse("it starts with a digit"));
}
if !text
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
return Err(refuse(
"it carries something other than ASCII letters, digits and `_`",
));
}
Ok(Self(text.to_owned()))
}
}
macro_rules! name_traits {
($name:ident, $schema:literal, $pattern:literal, $description:literal) => {
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
}
impl JsonSchema for $name {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed($schema)
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"description": $description,
"pattern": $pattern
})
}
}
};
}
name_traits!(
CodecName,
"CodecName",
"^[a-z][a-z0-9-]{0,63}$",
"A codec's name: a lowercase letter, then lowercase letters, digits and `-`."
);
name_traits!(
EnvName,
"EnvName",
"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
"The name of an environment variable: ASCII letters, digits and `_`, not starting with a digit."
);
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CodecConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub queue: Option<QueueName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reply_window_seconds: Option<NonZeroU64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_env: Option<EnvName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asker_env: Option<EnvName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_env: Option<EnvName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub about_env: Option<EnvName>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServeOptions {
pub asker: Option<Asker>,
pub about: Option<Address>,
pub session: Option<Duration>,
pub reply_window: Duration,
}
impl Default for ServeOptions {
fn default() -> Self {
Self {
asker: None,
about: None,
session: None,
reply_window: DEFAULT_REPLY_WINDOW,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecFailure {
Refused(String),
Failed(String),
}
pub trait Codec: Send {
fn name(&self) -> &CodecName;
fn answer(
&mut self,
frame: &str,
session: &mut ServeSession<'_>,
) -> Result<Value, CodecFailure>;
}
pub struct ServeSession<'a> {
bus: &'a Bus,
queue: &'a QueueName,
options: &'a ServeOptions,
asked: Vec<Pending<Value>>,
}
impl fmt::Debug for ServeSession<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ServeSession")
.field("queue", self.queue)
.field("options", self.options)
.field("asked", &self.asked.len())
.finish_non_exhaustive()
}
}
impl ServeSession<'_> {
#[must_use]
pub fn bus(&self) -> &Bus {
self.bus
}
#[must_use]
pub fn queue(&self) -> &QueueName {
self.queue
}
#[must_use]
pub fn options(&self) -> &ServeOptions {
self.options
}
pub fn raise(&mut self, record: Value) -> Result<Vec<(QueueName, Pushed<Value>)>, BusError> {
let record = match record {
Value::Object(mut fields) => {
if let Some(asker) = &self.options.asker {
fields
.entry("asker")
.or_insert_with(|| Value::String(asker.as_str().to_owned()));
}
if let Some(about) = &self.options.about {
fields
.entry(crate::ask::ABOUT)
.or_insert_with(|| Value::String(about.as_str().to_owned()));
}
Value::Object(fields)
}
other => other,
};
self.bus.send(self.queue, record)
}
pub fn ask(
&mut self,
question: Value,
blocking: bool,
) -> Result<(Correlation, Answer<Value>), BusError> {
let pending = self.bus.ask::<Value, Value>(
self.queue,
question,
AskOptions {
blocking,
asker: self.options.asker.clone(),
about: self.options.about.clone(),
},
)?;
let answer = pending.wait(self.options.reply_window);
let correlation = pending.correlation().clone();
self.asked.push(pending);
Ok((correlation, answer))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Served {
StreamEnded {
abandoned: usize,
},
SessionOver {
standing: usize,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ServeError {
#[error("{0}")]
Refused(String),
#[error("{0}")]
Failed(String),
#[error("the frame stream could not be read: {0}")]
Stream(std::io::Error),
#[error("the frame reader could not be started: {0}")]
Spawn(std::io::Error),
#[error("a response could not be written: {0}")]
Write(std::io::Error),
#[error(transparent)]
Bus(#[from] BusError),
}
impl Bus {
pub fn serve(
&self,
queue: &QueueName,
codec: &mut dyn Codec,
options: &ServeOptions,
input: Box<dyn BufRead + Send>,
output: &mut dyn Write,
) -> Result<Served, ServeError> {
self.queue(queue)?;
let deadline = options
.session
.and_then(|session| Instant::now().checked_add(session));
let (frames, arriving) = mpsc::channel();
std::thread::Builder::new()
.name("onemessagebus-frame-reader".to_owned())
.spawn(move || {
for line in input.lines() {
if frames.send(line).is_err() {
break;
}
}
})
.map_err(ServeError::Spawn)?;
let mut session = ServeSession {
bus: self,
queue,
options,
asked: Vec::new(),
};
let bound_reached = loop {
let line = match deadline {
Some(deadline) => {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
break true;
}
match arriving.recv_timeout(left) {
Ok(line) => line,
Err(RecvTimeoutError::Timeout) => break true,
Err(RecvTimeoutError::Disconnected) => break false,
}
}
None => match arriving.recv() {
Ok(line) => line,
Err(_) => break false,
},
};
let line = line.map_err(ServeError::Stream)?;
if line.trim().is_empty() {
continue;
}
let response =
codec
.answer(line.trim(), &mut session)
.map_err(|failure| match failure {
CodecFailure::Refused(why) => ServeError::Refused(why),
CodecFailure::Failed(why) => ServeError::Failed(why),
})?;
let mut written = serde_json::to_vec(&response)
.map_err(|failure| ServeError::Write(std::io::Error::other(failure)))?;
written.push(b'\n');
output
.write_all(&written)
.and_then(|()| output.flush())
.map_err(ServeError::Write)?;
};
let mut unanswered = Vec::new();
for pending in &session.asked {
if pending.reply_record().map_err(BusError::from)?.is_none() {
unanswered.push(pending);
}
}
if bound_reached {
return Ok(Served::SessionOver {
standing: unanswered.len(),
});
}
for pending in &unanswered {
pending.abandon().map_err(BusError::from)?;
}
Ok(Served::StreamEnded {
abandoned: unanswered.len(),
})
}
}