1use crate::cli::state::CliState;
30use clap::Subcommand;
31use haste_fhir_client::FHIRClient;
32use haste_fhir_converter::Input;
33use haste_fhir_model::r4::generated::resources::Resource;
34use haste_fhir_model::r4::generated::terminology::{BundleType, IssueType};
35use haste_fhir_operation_error::OperationOutcomeError;
36use haste_hl7v2::ack::{AckCode, InboundHeader, build_ack};
37use haste_hl7v2::mllp_async::{FrameEnd, encode_frame, read_frame};
38use std::collections::HashMap;
39use std::net::SocketAddr;
40use std::sync::Arc;
41use std::time::Duration;
42use tokio::io::AsyncWriteExt;
43use tokio::net::{TcpListener, TcpStream};
44use tokio::sync::{Mutex, Semaphore};
45
46#[derive(Subcommand, Debug)]
48pub(crate) enum HL7v2Commands {
49 Receiver {
51 #[arg(short, long, default_value = "0.0.0.0")]
53 address: String,
54 #[arg(short, long)]
56 port: u16,
57 #[arg(short, long)]
60 main: String,
61 #[arg(short, long)]
63 template_dir: String,
64 #[arg(long, default_value_t = 64)]
67 max_connections: usize,
68 #[arg(long, default_value_t = 300)]
71 idle_timeout_secs: u64,
72 #[arg(long, default_value_t = 30)]
74 read_timeout_secs: u64,
75 },
76 Sender {
78 #[arg(short, long)]
80 address: String,
81 #[arg(short, long)]
83 port: u16,
84 #[arg(short, long)]
86 file: Option<String>,
87 #[arg(long, default_value_t = 30)]
89 timeout_secs: u64,
90 },
91}
92
93struct ReceiverConfig {
95 template_dir: String,
96 main_template: String,
97 idle_timeout: Duration,
98 read_timeout: Duration,
99}
100
101struct MessageOutcome {
103 code: AckCode,
104 detail: Option<String>,
105}
106
107impl MessageOutcome {
108 fn accepted() -> Self {
109 Self {
110 code: AckCode::ApplicationAccept,
111 detail: None,
112 }
113 }
114
115 fn rejected(detail: impl Into<String>) -> Self {
117 Self {
118 code: AckCode::ApplicationReject,
119 detail: Some(detail.into()),
120 }
121 }
122
123 fn errored(detail: impl Into<String>) -> Self {
125 Self {
126 code: AckCode::ApplicationError,
127 detail: Some(detail.into()),
128 }
129 }
130}
131
132pub(crate) async fn run(
134 state: Arc<Mutex<CliState>>,
135 command: &HL7v2Commands,
136) -> Result<(), OperationOutcomeError> {
137 match command {
138 HL7v2Commands::Receiver {
139 address,
140 port,
141 main,
142 template_dir,
143 max_connections,
144 idle_timeout_secs,
145 read_timeout_secs,
146 } => {
147 let fhir_client = crate::cli::client::fhir_client(state).await?;
148
149 let environment = haste_fhir_converter::create_environment(Some(template_dir));
154 environment.get_template(main).map_err(|e| {
155 OperationOutcomeError::fatal(
156 IssueType::not_found(),
157 format!("Conversion template '{main}' not found in '{template_dir}': {e}"),
158 )
159 })?;
160
161 let config = Arc::new(ReceiverConfig {
162 template_dir: template_dir.clone(),
163 main_template: main.clone(),
164 idle_timeout: Duration::from_secs(*idle_timeout_secs),
165 read_timeout: Duration::from_secs(*read_timeout_secs),
166 });
167
168 serve(
169 &format!("{address}:{port}"),
170 fhir_client,
171 config,
172 *max_connections,
173 )
174 .await
175 }
176 HL7v2Commands::Sender {
177 address,
178 port,
179 file,
180 timeout_secs,
181 } => {
182 send_message(
183 &format!("{address}:{port}"),
184 file.as_deref(),
185 Duration::from_secs(*timeout_secs),
186 )
187 .await
188 }
189 }
190}
191
192async fn serve<Client>(
194 bind: &str,
195 fhir_client: Arc<Client>,
196 config: Arc<ReceiverConfig>,
197 max_connections: usize,
198) -> Result<(), OperationOutcomeError>
199where
200 Client: FHIRClient<(), OperationOutcomeError> + Send + Sync + 'static,
201{
202 let listener = TcpListener::bind(bind).await.map_err(|e| {
203 OperationOutcomeError::fatal(
204 IssueType::exception(),
205 format!("Failed to bind the MLLP listener to {bind}: {e}"),
206 )
207 })?;
208
209 let local = listener
210 .local_addr()
211 .map_or_else(|_| bind.to_string(), |a| a.to_string());
212 tracing::info!(
213 address = %local,
214 max_connections,
215 template = %config.main_template,
216 "MLLP listener started"
217 );
218
219 let permits = Arc::new(Semaphore::new(max_connections));
223 let in_flight = Arc::new(tokio::sync::RwLock::new(()));
225
226 loop {
227 tokio::select! {
228 biased;
231
232 _ = shutdown_signal() => {
233 tracing::info!("Shutdown signalled; no longer accepting connections");
234 break;
235 }
236
237 accepted = listener.accept() => {
238 let (stream, peer) = match accepted {
239 Ok(pair) => pair,
240 Err(e) => {
241 tracing::warn!("Failed to accept an MLLP connection: {e}");
245 continue;
246 }
247 };
248
249 let Ok(permit) = permits.clone().acquire_owned().await else {
250 break;
251 };
252
253 let fhir_client = fhir_client.clone();
254 let config = config.clone();
255 let guard = in_flight.clone();
256
257 tokio::spawn(async move {
258 let _permit = permit;
259 let _guard = guard.read().await;
260 handle_connection(stream, peer, fhir_client, config).await;
261 });
262 }
263 }
264 }
265
266 tracing::info!("Draining in-flight messages");
270 std::mem::drop(in_flight.write().await);
271 tracing::info!("MLLP listener stopped");
272
273 Ok(())
274}
275
276async fn shutdown_signal() {
278 let interrupt = tokio::signal::ctrl_c();
279
280 #[cfg(unix)]
281 {
282 let mut terminate =
283 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
284 Ok(signal) => signal,
285 Err(e) => {
286 tracing::warn!("Could not listen for SIGTERM: {e}");
287 let _ = interrupt.await;
288 return;
289 }
290 };
291
292 tokio::select! {
293 _ = interrupt => {}
294 _ = terminate.recv() => {}
295 }
296 }
297
298 #[cfg(not(unix))]
299 {
300 let _ = interrupt.await;
301 }
302}
303
304async fn handle_connection<Client>(
306 mut stream: TcpStream,
307 peer: SocketAddr,
308 fhir_client: Arc<Client>,
309 config: Arc<ReceiverConfig>,
310) where
311 Client: FHIRClient<(), OperationOutcomeError> + Send + Sync + 'static,
312{
313 if let Err(e) = stream.set_nodelay(true) {
316 tracing::debug!(%peer, "Could not disable Nagle: {e}");
317 }
318
319 tracing::debug!(%peer, "MLLP connection opened");
320 let mut handled: u64 = 0;
321
322 loop {
323 let frame = match tokio::time::timeout(config.idle_timeout, read_frame(&mut stream)).await {
328 Err(_) => {
329 tracing::debug!(%peer, handled, "Closing idle MLLP connection");
330 break;
331 }
332 Ok(Ok(FrameEnd::Closed)) => {
333 tracing::debug!(%peer, handled, "MLLP connection closed by peer");
334 break;
335 }
336 Ok(Ok(FrameEnd::Frame(payload))) => payload,
337 Ok(Err(e)) => {
338 tracing::warn!(%peer, handled, "MLLP framing error, closing connection: {e}");
342 break;
343 }
344 };
345
346 let message = String::from_utf8_lossy(&frame).to_string();
347 let header = InboundHeader::scan(&message);
348 let control_id = header.control_id.clone();
349
350 let outcome = tokio::time::timeout(
351 config.read_timeout,
352 handle_message(&message, fhir_client.as_ref(), config.as_ref()),
353 )
354 .await
355 .unwrap_or_else(|_| {
356 MessageOutcome::errored("Timed out converting or storing the message; please retry")
357 });
358
359 match outcome.code {
360 AckCode::ApplicationAccept => {
361 tracing::info!(%peer, control_id = %control_id, "Accepted HL7v2 message");
362 }
363 AckCode::ApplicationReject => {
364 tracing::warn!(
365 %peer, control_id = %control_id,
366 detail = outcome.detail.as_deref().unwrap_or(""),
367 "Rejected HL7v2 message"
368 );
369 }
370 AckCode::ApplicationError => {
371 tracing::error!(
372 %peer, control_id = %control_id,
373 detail = outcome.detail.as_deref().unwrap_or(""),
374 "Failed to store HL7v2 message"
375 );
376 }
377 }
378
379 let ack = build_ack(
380 &header,
381 outcome.code,
382 outcome.detail.as_deref(),
383 ×tamp(),
384 );
385
386 if let Err(e) = stream.write_all(&encode_frame(ack.as_bytes())).await {
387 tracing::warn!(%peer, control_id = %control_id, "Failed to send ACK: {e}");
390 break;
391 }
392 if let Err(e) = stream.flush().await {
393 tracing::warn!(%peer, control_id = %control_id, "Failed to flush ACK: {e}");
394 break;
395 }
396
397 handled += 1;
398 }
399}
400
401async fn handle_message<Client>(
406 message: &str,
407 fhir_client: &Client,
408 config: &ReceiverConfig,
409) -> MessageOutcome
410where
411 Client: FHIRClient<(), OperationOutcomeError> + Send + Sync,
412{
413 if message.trim().is_empty() {
414 return MessageOutcome::rejected("Empty message");
415 }
416
417 let hl7v2 = match haste_fhir_converter::convert_input(Input::HL7V2(message.to_string())) {
420 Ok(value) => value,
421 Err(e) => return MessageOutcome::rejected(format!("Could not parse the message: {e}")),
422 };
423
424 let environment = haste_fhir_converter::create_environment(Some(&config.template_dir));
428 let Ok(template) = environment.get_template(&config.main_template) else {
429 return MessageOutcome::errored(format!(
430 "Conversion template '{}' is no longer available",
431 config.main_template
432 ));
433 };
434
435 let mut ctx = HashMap::new();
436 ctx.insert("hl7v2", hl7v2);
437
438 let output = match haste_fhir_converter::transform(
439 &template,
440 ctx,
441 &haste_fhir_converter::OutputFormat::FHIR,
442 ) {
443 Ok(output) => output,
444 Err(e) => {
445 return MessageOutcome::rejected(format!("Could not convert the message to FHIR: {e}"));
449 }
450 };
451
452 let haste_fhir_converter::Output::FHIR(resource) = output else {
453 return MessageOutcome::errored("The conversion template did not produce a FHIR resource");
454 };
455
456 submit(*resource, fhir_client).await
457}
458
459async fn submit<Client>(resource: Resource, fhir_client: &Client) -> MessageOutcome
461where
462 Client: FHIRClient<(), OperationOutcomeError> + Send + Sync,
463{
464 let stored = match resource {
467 Resource::Bundle(bundle) => match &bundle.type_ {
468 t if t == &BundleType::transaction() => fhir_client.transaction((), bundle).await,
469 t if t == &BundleType::batch() => fhir_client.batch((), bundle).await,
470 other => {
471 return MessageOutcome::rejected(format!(
472 "A converted Bundle must be a transaction or a batch, got {:?}",
473 other.as_str().unwrap_or("(none)")
474 ));
475 }
476 },
477 resource => {
478 let resource_type = resource.resource_type();
479 fhir_client
480 .create((), resource_type, resource)
481 .await
482 .map(|_| Default::default())
483 }
484 };
485
486 match stored {
487 Ok(_) => MessageOutcome::accepted(),
488 Err(e) => MessageOutcome::errored(format!("Could not store the message: {e}")),
489 }
490}
491
492fn timestamp() -> String {
494 chrono::Utc::now().format("%Y%m%d%H%M%S").to_string()
495}
496
497async fn send_message(
499 target: &str,
500 file: Option<&str>,
501 timeout: Duration,
502) -> Result<(), OperationOutcomeError> {
503 let message = match file {
504 Some(path) => tokio::fs::read_to_string(path).await.map_err(|e| {
505 OperationOutcomeError::fatal(
506 IssueType::not_found(),
507 format!("Could not read '{path}': {e}"),
508 )
509 })?,
510 None => {
511 use std::io::Read as _;
512 let mut buf = String::new();
513 std::io::stdin().read_to_string(&mut buf).map_err(|e| {
514 OperationOutcomeError::fatal(
515 IssueType::exception(),
516 format!("Could not read the message from stdin: {e}"),
517 )
518 })?;
519 buf
520 }
521 };
522
523 let message = normalize_segment_separators(&message);
527
528 let mut stream = tokio::time::timeout(timeout, TcpStream::connect(target))
529 .await
530 .map_err(|_| {
531 OperationOutcomeError::fatal(
532 IssueType::timeout(),
533 format!("Timed out connecting to {target}"),
534 )
535 })?
536 .map_err(|e| {
537 OperationOutcomeError::fatal(
538 IssueType::exception(),
539 format!("Could not connect to {target}: {e}"),
540 )
541 })?;
542
543 let _ = stream.set_nodelay(true);
544
545 stream
546 .write_all(&encode_frame(message.as_bytes()))
547 .await
548 .map_err(|e| {
549 OperationOutcomeError::fatal(
550 IssueType::exception(),
551 format!("Could not send the message: {e}"),
552 )
553 })?;
554 stream.flush().await.map_err(|e| {
555 OperationOutcomeError::fatal(
556 IssueType::exception(),
557 format!("Could not flush the message: {e}"),
558 )
559 })?;
560
561 let frame = tokio::time::timeout(timeout, read_frame(&mut stream))
562 .await
563 .map_err(|_| {
564 OperationOutcomeError::fatal(
565 IssueType::timeout(),
566 format!("No acknowledgement from {target} within {timeout:?}"),
567 )
568 })??;
569
570 let FrameEnd::Frame(payload) = frame else {
571 return Err(OperationOutcomeError::fatal(
572 IssueType::incomplete(),
573 "The receiver closed the connection without acknowledging".to_string(),
574 ));
575 };
576
577 let ack = String::from_utf8_lossy(&payload).to_string();
578 println!("{ack}");
579
580 if ack_was_accepted(&ack) {
583 Ok(())
584 } else {
585 Err(OperationOutcomeError::error(
586 IssueType::processing(),
587 format!("The receiver did not accept the message: {}", ack.trim()),
588 ))
589 }
590}
591
592fn normalize_segment_separators(message: &str) -> String {
594 message.replace("\r\n", "\r").replace('\n', "\r")
595}
596
597fn ack_was_accepted(ack: &str) -> bool {
599 ack.split(['\r', '\n'])
600 .find(|segment| segment.starts_with("MSA"))
601 .and_then(|msa| msa.split('|').nth(1))
602 .is_some_and(|code| code == "AA" || code == "CA")
603}