use std::collections::BTreeMap;
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, FieldPath, Pushed};
use crate::schema::SchemaId;
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, PartialEq, 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 about_env: Option<EnvName>,
#[schemars(with = "FieldPath")]
pub select: String,
pub frames: BTreeMap<String, FrameConfig>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FrameConfig {
pub schema: SchemaId,
pub bindings: Vec<Binding>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Binding {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<FieldEquals>,
#[serde(flatten)]
pub action: BindingAction,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FieldEquals {
#[schemars(with = "FieldPath")]
pub field: String,
pub equals: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "do", rename_all = "lowercase", deny_unknown_fields)]
pub enum BindingAction {
Answer {
response: Value,
},
Refuse {
message: String,
},
Raise {
record: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
response: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
fail: Option<String>,
},
Ask {
record: Value,
#[serde(default)]
blocking: bool,
response: Value,
unanswered: Value,
},
}
#[derive(Debug, Clone)]
pub struct ConfiguredCodec {
name: CodecName,
config: CodecConfig,
}
impl ConfiguredCodec {
pub fn new(name: CodecName, config: CodecConfig) -> Result<Self, String> {
validate_codec(&name, &config)?;
Ok(Self { name, config })
}
}
fn path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
path.split('.').try_fold(root, |value, key| {
(!key.is_empty())
.then(|| value.as_object()?.get(key))
.flatten()
})
}
fn valid_path(value: &str) -> bool {
!value.is_empty() && value.split('.').all(|part| !part.is_empty())
}
fn placeholders(text: &str) -> Result<Vec<(String, String)>, String> {
let mut found = Vec::new();
let bytes = text.as_bytes();
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'{' if bytes.get(index + 1) == Some(&b'{') => index += 2,
b'}' if bytes.get(index + 1) == Some(&b'}') => index += 2,
b'{' => {
let end = text[index + 1..]
.find('}')
.map(|offset| index + 1 + offset)
.ok_or_else(|| "an opening `{` has no closing `}`".to_owned())?;
let placeholder = &text[index + 1..end];
let (root, field) = placeholder.split_once('.').ok_or_else(|| {
format!("placeholder `{{{placeholder}}}` has no root and path")
})?;
if !matches!(root, "frame" | "reply") || !valid_path(field) {
return Err(format!(
"placeholder `{{{placeholder}}}` is not a frame or reply path"
));
}
found.push((root.to_owned(), field.to_owned()));
index = end + 1;
}
b'}' => return Err("an unescaped `}` has no opening `{`".to_owned()),
_ => index += 1,
}
}
Ok(found)
}
fn visit_strings(value: &Value, allow_reply: bool) -> Result<(), String> {
match value {
Value::String(text) => {
for (root, _) in placeholders(text)? {
if root == "reply" && !allow_reply {
return Err(
"a `reply` placeholder is only allowed in an ask response".to_owned()
);
}
}
}
Value::Array(values) => {
for value in values {
visit_strings(value, allow_reply)?;
}
}
Value::Object(fields) => {
for value in fields.values() {
visit_strings(value, allow_reply)?;
}
}
_ => {}
}
Ok(())
}
pub(crate) fn validate_codec(name: &CodecName, config: &CodecConfig) -> Result<(), String> {
if !valid_path(&config.select) {
return Err(format!(
"codecs.{name}.select is not a dot-separated object path"
));
}
if config.frames.is_empty() {
return Err(format!("codecs.{name}.frames is empty"));
}
for (entry, frame) in &config.frames {
if frame.bindings.is_empty() {
return Err(format!("codecs.{name}.frames.{entry}.bindings is empty"));
}
for (index, binding) in frame.bindings.iter().enumerate() {
let at = format!("codecs.{name}.frames.{entry}.bindings[{index}]");
if let Some(condition) = &binding.when {
if !valid_path(&condition.field) {
return Err(format!(
"{at}.when.field is not a dot-separated object path"
));
}
if condition.equals.is_array() || condition.equals.is_object() {
return Err(format!("{at}.when.equals is not a JSON scalar"));
}
}
let check =
|value, reply| visit_strings(value, reply).map_err(|why| format!("{at}: {why}"));
match &binding.action {
BindingAction::Answer { response } => check(response, false)?,
BindingAction::Refuse { message } => check(&Value::String(message.clone()), false)?,
BindingAction::Raise {
record,
response,
fail,
} => {
check(record, false)?;
match (response, fail) {
(Some(response), None) => check(response, false)?,
(None, Some(fail)) => check(&Value::String(fail.clone()), false)?,
_ => {
return Err(format!(
"{at}: raise takes exactly one of `response` and `fail`"
))
}
}
}
BindingAction::Ask {
record,
response,
unanswered,
..
} => {
check(record, false)?;
check(response, true)?;
check(unanswered, false)?;
validate_mappings(response).map_err(|why| format!("{at}.response: {why}"))?;
}
}
}
}
Ok(())
}
fn validate_mappings(value: &Value) -> Result<(), String> {
match value {
Value::Array(values) => values.iter().try_for_each(validate_mappings),
Value::Object(fields) if fields.contains_key("from") => {
if !fields
.keys()
.all(|key| matches!(key.as_str(), "from" | "default"))
{
return Err("a mapping has a key other than `from` and `default`".to_owned());
}
let paths: Vec<&str> = match &fields["from"] {
Value::String(path) => vec![path],
Value::Array(paths) if !paths.is_empty() => paths
.iter()
.map(|path| {
path.as_str()
.ok_or_else(|| "a `from` list contains a non-string path".to_owned())
})
.collect::<Result<_, _>>()?,
_ => return Err("`from` is not a path or non-empty list of paths".to_owned()),
};
if paths
.iter()
.any(|path| !path.starts_with("reply.") || !valid_path(&path[6..]))
{
return Err("a `from` path is not under `reply`".to_owned());
}
if let Some(default) = fields.get("default") {
visit_strings(default, true)?;
}
Ok(())
}
Value::Object(fields) => fields.values().try_for_each(validate_mappings),
_ => Ok(()),
}
}
fn render_string(text: &str, frame: &Value, reply: Option<&Value>) -> Result<Value, String> {
let fields = placeholders(text)?;
if fields.len() == 1 {
let (root, field) = &fields[0];
let exact = format!("{{{root}.{field}}}");
if text == exact {
return Ok(match root.as_str() {
"frame" => path(frame, field)
.cloned()
.unwrap_or(Value::String(String::new())),
"reply" => reply
.and_then(|value| path(value, field))
.cloned()
.unwrap_or(Value::String(String::new())),
_ => unreachable!(),
});
}
}
let mut rendered = String::new();
let bytes = text.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'{' && bytes.get(index + 1) == Some(&b'{') {
rendered.push('{');
index += 2;
continue;
}
if bytes[index] == b'}' && bytes.get(index + 1) == Some(&b'}') {
rendered.push('}');
index += 2;
continue;
}
if bytes[index] == b'{' {
let end = text[index + 1..]
.find('}')
.map(|offset| index + 1 + offset)
.expect("validated template");
let placeholder = &text[index + 1..end];
let (root, field) = placeholder.split_once('.').expect("validated placeholder");
let value = match root {
"frame" => path(frame, field),
"reply" => reply.and_then(|value| path(value, field)),
_ => None,
};
if let Some(value) = value {
match value {
Value::String(text) => rendered.push_str(text),
other => rendered.push_str(&other.to_string()),
}
}
index = end + 1;
} else {
let character = text[index..].chars().next().expect("inside a string");
rendered.push(character);
index += character.len_utf8();
}
}
Ok(Value::String(rendered))
}
fn render(value: &Value, frame: &Value, reply: Option<&Value>) -> Result<Value, String> {
Ok(match value {
Value::String(text) => render_string(text, frame, reply)?,
Value::Array(values) => Value::Array(
values
.iter()
.map(|value| render(value, frame, reply))
.collect::<Result<_, _>>()?,
),
Value::Object(fields) => Value::Object(
fields
.iter()
.map(|(key, value)| Ok((key.clone(), render(value, frame, reply)?)))
.collect::<Result<_, String>>()?,
),
other => other.clone(),
})
}
fn ruling(value: &Value) -> &Value {
value
.as_object()
.and_then(|fields| fields.get("reply"))
.unwrap_or(value)
}
fn resolve_ask(value: &Value, frame: &Value, reply: &Value) -> Result<Option<Value>, String> {
if let Value::Array(values) = value {
let mut output = Vec::with_capacity(values.len());
for value in values {
let Some(value) = resolve_ask(value, frame, reply)? else {
return Ok(None);
};
output.push(value);
}
return Ok(Some(Value::Array(output)));
}
let Value::Object(fields) = value else {
return render(value, frame, Some(reply)).map(Some);
};
let is_mapping = fields.contains_key("from")
&& fields
.keys()
.all(|key| matches!(key.as_str(), "from" | "default"));
if is_mapping {
let paths = match &fields["from"] {
Value::String(path) => vec![path.as_str()],
Value::Array(paths) => paths
.iter()
.map(|path| {
path.as_str().ok_or_else(|| {
"an ask response `from` list contains a non-string path".to_owned()
})
})
.collect::<Result<Vec<_>, _>>()?,
_ => return Err("an ask response `from` is not a path or list of paths".to_owned()),
};
for candidate in paths {
let candidate = candidate.strip_prefix("reply.").unwrap_or(candidate);
if let Some(value) = path(reply, candidate).filter(|value| value.as_str() != Some("")) {
return Ok(Some(value.clone()));
}
}
return fields
.get("default")
.map(|value| render(value, frame, Some(reply)))
.transpose();
}
let mut output = serde_json::Map::new();
for (key, value) in fields {
let Some(value) = resolve_ask(value, frame, reply)? else {
return Ok(None);
};
output.insert(key.clone(), value);
}
Ok(Some(Value::Object(output)))
}
impl Codec for ConfiguredCodec {
fn name(&self) -> &CodecName {
&self.name
}
fn answer(
&mut self,
text: &str,
session: &mut ServeSession<'_>,
) -> Result<Value, CodecFailure> {
let frame: Value = serde_json::from_str(text).map_err(|failure| {
CodecFailure::Refused(format!("the frame is not JSON: {failure}"))
})?;
if !frame.is_object() {
return Err(CodecFailure::Refused(
"the frame is not a JSON object".to_owned(),
));
}
let selected = path(&frame, &self.config.select);
let word = selected.and_then(Value::as_str).ok_or_else(|| {
CodecFailure::Refused(format!(
"the frame's `{}` is absent or not a string; declared entries: {}",
self.config.select,
self.config
.frames
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
))
})?;
let entry = self.config.frames.get(word).ok_or_else(|| {
CodecFailure::Refused(format!(
"the frame's `{}` is `{word}`, not one of the declared entries: {}",
self.config.select,
self.config
.frames
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
))
})?;
session
.bus()
.registry()
.check(&entry.schema, &frame)
.map_err(|failure| {
CodecFailure::Refused(format!(
"the frame does not validate against {}: {failure}",
entry.schema
))
})?;
let binding = entry
.bindings
.iter()
.find(|binding| {
binding.when.as_ref().is_none_or(|condition| {
path(&frame, &condition.field) == Some(&condition.equals)
})
})
.ok_or_else(|| CodecFailure::Refused(format!("no binding holds for entry `{word}`")))?;
let rendered = |value: &Value| render(value, &frame, None).map_err(CodecFailure::Refused);
match &binding.action {
BindingAction::Answer { response } => rendered(response),
BindingAction::Refuse { message } => Err(CodecFailure::Refused(
rendered(&Value::String(message.clone()))?
.as_str()
.unwrap_or_default()
.to_owned(),
)),
BindingAction::Raise {
record,
response,
fail,
} => {
session.raise(rendered(record)?).map_err(|failure| {
CodecFailure::Failed(format!("the queue refused the raised record: {failure}"))
})?;
match (response, fail) {
(Some(response), None) => rendered(response),
(None, Some(fail)) => Err(CodecFailure::Failed(
rendered(&Value::String(fail.clone()))?
.as_str()
.unwrap_or_default()
.to_owned(),
)),
_ => unreachable!("validated configuration"),
}
}
BindingAction::Ask {
record,
blocking,
response,
unanswered,
} => {
let (_, answer) = session
.ask(rendered(record)?, *blocking)
.map_err(|failure| {
CodecFailure::Failed(format!("the queue refused the question: {failure}"))
})?;
match answer {
Answer::Reply(reply) => {
let reply = ruling(&reply);
resolve_ask(response, &frame, reply)
.map_err(CodecFailure::Refused)?
.map_or_else(|| rendered(unanswered), Ok)
}
Answer::Timeout | Answer::Abandoned => rendered(unanswered),
Answer::Refused(failure) => Err(CodecFailure::Failed(format!(
"the answer was refused: {}",
failure.reason
))),
}
}
}
}
}
#[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(),
})
}
}