1#![deny(missing_docs)]
2#![deny(rust_2018_idioms)]
3#![deny(rustdoc::broken_intra_doc_links)]
4
5mod auth;
36mod config;
37mod jobs;
38mod kv;
39mod pubsub;
40mod request_reply;
41mod setup;
42
43pub use auth::NatsAuth;
44pub use config::NatsBusConfig;
45pub use jobs::NatsJobQueue;
46pub use kv::NatsKv;
47pub use pubsub::NatsPubsub;
48pub use request_reply::NatsRequestReply;
49
50use async_nats::jetstream;
51use klieo_core::bus::{BusHandles, JobQueue, KvStore, Pubsub, RequestReply};
52use klieo_core::error::BusError;
53use std::sync::Arc;
54
55pub struct NatsBus {
59 pub pubsub: Arc<dyn Pubsub>,
61 pub request_reply: Arc<dyn RequestReply>,
63 pub kv: Arc<dyn KvStore>,
65 pub jobs: Arc<dyn JobQueue>,
67}
68
69impl std::fmt::Debug for NatsBus {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("NatsBus").finish_non_exhaustive()
72 }
73}
74
75impl NatsBus {
76 pub async fn connect_url(url: impl Into<String>) -> Result<Self, BusError> {
81 Self::connect(NatsBusConfig {
82 url: url.into(),
83 ..NatsBusConfig::default()
84 })
85 .await
86 }
87
88 pub async fn connect(config: NatsBusConfig) -> Result<Self, BusError> {
93 Self::connect_with_auth(config, NatsAuth::default()).await
94 }
95
96 pub async fn connect_with_auth(
101 config: NatsBusConfig,
102 auth: NatsAuth,
103 ) -> Result<Self, BusError> {
104 let opts = apply_auth(async_nats::ConnectOptions::new(), &auth, &config.url).await?;
105 let client = opts
106 .connect(&config.url)
107 .await
108 .map_err(|e| BusError::Connection(format!("nats {}: {}", config.url, e)))?;
109 Ok(Self::from_client(client, config))
110 }
111
112 fn from_client(client: async_nats::Client, config: NatsBusConfig) -> Self {
115 let js = jetstream::new(client.clone());
118
119 let kv_concrete = Arc::new(NatsKv::new(js.clone(), &config));
120 let pubsub_concrete = Arc::new(NatsPubsub::new(
121 js.clone(),
122 config.app_prefix.clone(),
123 config.default_ack_wait,
124 crate::setup::StreamLimits::from_config(&config),
125 ));
126 let rr_concrete = Arc::new(NatsRequestReply::new(client));
127 let jobs_concrete = Arc::new(NatsJobQueue::new(js, kv_concrete.clone(), &config));
128
129 Self {
130 pubsub: pubsub_concrete,
131 request_reply: rr_concrete,
132 kv: kv_concrete,
133 jobs: jobs_concrete,
134 }
135 }
136}
137
138async fn apply_auth(
142 mut opts: async_nats::ConnectOptions,
143 auth: &NatsAuth,
144 url: &str,
145) -> Result<async_nats::ConnectOptions, BusError> {
146 use secrecy::ExposeSecret;
147
148 if let Some(token) = &auth.token {
149 opts = opts.token(token.expose_secret().to_string());
150 }
151 if let Some((user, password)) = &auth.user_password {
152 opts = opts.user_and_password(user.clone(), password.expose_secret().to_string());
153 }
154 if let Some(path) = &auth.credentials_file {
155 opts = opts.credentials_file(path).await.map_err(|e| {
156 BusError::Connection(format!(
157 "nats {url}: credentials file {}: {e}",
158 path.display()
159 ))
160 })?;
161 }
162 if auth.tls {
163 opts = opts.require_tls(true);
164 }
165 Ok(opts)
166}
167
168impl From<NatsBus> for BusHandles {
169 fn from(value: NatsBus) -> Self {
170 BusHandles::new(value.pubsub, value.kv, value.request_reply, value.jobs)
171 }
172}
173
174#[cfg(test)]
175mod lib_tests {
176 use super::*;
177 use klieo_core::error::BusError;
178
179 #[tokio::test]
180 async fn connect_to_unreachable_server_returns_connection_error() {
181 let cfg = NatsBusConfig {
182 url: "nats://127.0.0.1:14222".to_string(), ..NatsBusConfig::default()
184 };
185 let err = NatsBus::connect(cfg).await.unwrap_err();
186 assert!(matches!(err, BusError::Connection(_)), "got {err:?}");
187 }
188
189 #[tokio::test]
190 async fn connect_url_propagates_connection_error_on_unreachable_server() {
191 let err = NatsBus::connect_url("nats://127.0.0.1:14222")
192 .await
193 .unwrap_err();
194 assert!(matches!(err, BusError::Connection(_)), "got {err:?}");
195 }
196}