mod bus;
mod delivery;
mod routes;
mod subject;
use std::str::FromStr;
use std::sync::Arc;
use async_nats::{ConnectOptions, ServerAddr};
use tokio::runtime::Handle;
pub(crate) use bus::{Bus, Origin, PublishMode, Settings};
pub(crate) use delivery::{Consumer, Feed, JetStreamDelivery, Release};
pub(crate) use routes::{Route, RouteBook};
use crate::error::NatsError;
use crate::subject::{NatsSubscription, SubscriptionPlan};
use crate::subscriber::NatsSubscriber;
use bus::ConsumerSpec;
use subject::{SubjectPattern, check_queue_group};
pub(crate) fn connect(addrs: &str, options: &ConnectOptions) -> Result<Arc<Bus>, NatsError> {
for addr in addrs.split(',') {
ServerAddr::from_str(addr.trim()).map_err(|err| NatsError::Connect(Box::new(err)))?;
}
Ok(Bus::new(settings(options)?, Handle::current()))
}
pub(crate) fn settings(options: &ConnectOptions) -> Result<Settings, NatsError> {
let printed = format!("{options:?}");
let field = |name: &str| -> Result<String, NatsError> {
let key = format!("\"{name}\": ");
let start = printed.find(&key).ok_or_else(|| {
NatsError::Connect(
format!("the client's connect options no longer state `{name}`").into(),
)
})? + key.len();
let rest = &printed[start..];
let Some(quoted) = rest.strip_prefix('"') else {
return Ok(rest[..rest.find([',', '}']).unwrap_or(rest.len())].to_owned());
};
let mut value = String::new();
let mut chars = quoted.chars();
while let Some(c) = chars.next() {
match c {
'"' => return Ok(value),
'\\' => value.push(chars.next().unwrap_or('\\')),
c => value.push(c),
}
}
Err(NatsError::Connect(
format!("the client's connect options state `{name}` unterminated").into(),
))
};
let no_echo = field("no_echo")? == "true";
let inbox_prefix = field("inbox_prefix")?;
Ok(Settings {
no_echo,
inbox_prefix,
})
}
fn check_name(what: &str, name: &str) -> Result<(), NatsError> {
if name.is_empty()
|| name
.chars()
.any(|c| c.is_whitespace() || matches!(c, '.' | '*' | '>' | '/' | '\\'))
{
return Err(NatsError::JetStream(
format!("invalid {what} name `{name}`").into(),
));
}
Ok(())
}
pub(crate) fn route<S: NatsSubscription>(source: &S) -> Option<Route> {
let subject = source.subject();
match source.plan() {
SubscriptionPlan::Core { queue_group } => SubjectPattern::parse(subject)
.ok()
.map(|pattern| Route::core(subject, pattern, queue_group)),
SubscriptionPlan::JetStream {
stream,
durable,
filter_subject,
..
} => SubjectPattern::parse(filter_subject)
.ok()
.map(|filter| Route::jetstream(subject, filter, stream, durable)),
}
}
pub(crate) fn subscribe<S: NatsSubscription>(
bus: &Arc<Bus>,
book: &RouteBook,
source: &S,
) -> Result<NatsSubscriber, NatsError> {
let subject = source.subject();
bus.ensure_live(subject)?;
match source.plan() {
SubscriptionPlan::Core { queue_group } => {
let pattern = SubjectPattern::parse(subject)
.map_err(|err| NatsError::Subscribe(Box::new(err)))?;
if let Some(queue) = queue_group {
check_queue_group(queue).map_err(|err| NatsError::Subscribe(Box::new(err)))?;
}
book.record(Route::core(subject, pattern.clone(), queue_group));
let feed = bus.subscribe_core(pattern, queue_group);
Ok(NatsSubscriber::in_process_core(subject.to_owned(), feed))
}
SubscriptionPlan::JetStream {
stream,
durable,
filter_subject,
ack_wait,
deliver_policy,
..
} => {
check_name("stream", stream)?;
if let Some(durable) = durable {
check_name("durable", durable)?;
}
let filter = SubjectPattern::parse(filter_subject)
.map_err(|err| NatsError::JetStream(Box::new(err)))?;
book.record(Route::jetstream(subject, filter.clone(), stream, durable));
let (feed, requeue, consumer) = bus.subscribe_jetstream(ConsumerSpec {
stream,
durable,
filter,
deliver_policy,
});
Ok(NatsSubscriber::in_process_jetstream(
subject.to_owned(),
Consumer::new(feed, requeue, consumer, ack_wait),
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_settings_are_the_ones_the_options_state() {
let defaults = settings(&ConnectOptions::new()).expect("the options state both");
assert!(!defaults.no_echo);
assert_eq!(defaults.inbox_prefix, "_INBOX");
let set = settings(&ConnectOptions::new().no_echo().custom_inbox_prefix("_SVC"))
.expect("the options state both");
assert!(set.no_echo);
assert_eq!(set.inbox_prefix, "_SVC");
let escaped = settings(&ConnectOptions::new().custom_inbox_prefix(r#"_S"V\C"#))
.expect("the options state both");
assert_eq!(escaped.inbox_prefix, r#"_S"V\C"#);
}
#[tokio::test]
async fn an_address_the_client_refuses_is_refused() {
assert!(connect("nats://localhost:4222", &ConnectOptions::new()).is_ok());
assert!(connect("nats://a:4222, nats://b:4222", &ConnectOptions::new()).is_ok());
assert!(matches!(
connect("http://localhost:4222", &ConnectOptions::new()),
Err(NatsError::Connect(_))
));
}
}