use crate::server::AppState;
use hl7v2::{
Comp as RustComp, Field as RustField, Message as RustMessage, Rep as RustRep,
Segment as RustSegment, parse as rust_parse,
};
use std::sync::Arc;
use tonic::{Request, Response, Status};
#[expect(
missing_docs,
reason = "protobuf bindings are generated by tonic/prost"
)]
pub mod proto {
tonic::include_proto!("hl7v2.v1");
}
use proto::hl7_service_server::Hl7Service;
use proto::*;
pub struct Hl7ServiceImpl {
#[expect(
dead_code,
reason = "service state is retained for runtime parity and future streaming support"
)]
state: Arc<AppState>,
}
impl Hl7ServiceImpl {
pub fn new(state: Arc<AppState>) -> Self {
Self { state }
}
}
#[tonic::async_trait]
impl Hl7Service for Hl7ServiceImpl {
async fn parse(
&self,
request: Request<ParseRequest>,
) -> Result<Response<ParseResponse>, Status> {
let req = request.into_inner();
let parse_result = if req.mllp_framed {
match hl7v2::unwrap_mllp(&req.message) {
Ok(hl7) => rust_parse(hl7),
Err(e) => {
return Ok(Response::new(ParseResponse {
success: false,
message: None,
errors: vec![Error {
code: "MLLP_ERROR".to_string(),
message: format!("Failed to unwrap MLLP: {}", e),
details: std::collections::HashMap::new(),
trace_id: String::new(),
}],
metadata: None,
}));
}
}
} else {
rust_parse(&req.message)
};
match parse_result {
Ok(msg) => {
let metadata = extract_grpc_metadata(&msg);
let proto_msg = proto::Message::from(msg);
Ok(Response::new(ParseResponse {
success: true,
message: Some(proto_msg),
errors: Vec::new(),
metadata: Some(metadata),
}))
}
Err(e) => Ok(Response::new(ParseResponse {
success: false,
message: None,
errors: vec![Error {
code: "PARSE_ERROR".to_string(),
message: format!("Failed to parse HL7: {}", e),
details: std::collections::HashMap::new(),
trace_id: String::new(),
}],
metadata: None,
})),
}
}
type ParseStreamStream =
tokio_stream::wrappers::ReceiverStream<Result<ParseStreamResponse, Status>>;
async fn parse_stream(
&self,
_request: Request<tonic::Streaming<ParseStreamRequest>>,
) -> Result<Response<Self::ParseStreamStream>, Status> {
Err(Status::unimplemented("Streaming parse not yet implemented"))
}
async fn validate(
&self,
request: Request<ValidateRequest>,
) -> Result<Response<ValidateResponse>, Status> {
let req = request.into_inner();
let message = rust_parse(&req.message)
.map_err(|e| Status::invalid_argument(format!("Failed to parse HL7: {}", e)))?;
let profile = hl7v2::load_profile(&req.profile)
.map_err(|e| Status::invalid_argument(format!("Failed to load profile: {}", e)))?;
let issues = hl7v2::validate(&message, &profile);
let valid = issues.iter().all(|i| i.severity != hl7v2::Severity::Error);
let mut errors = Vec::new();
let mut warnings = Vec::new();
for issue in issues {
let location = issue.path.map(|p| {
let mut loc = Location::default();
let parts: Vec<&str> = p.split('.').collect();
if !parts.is_empty() {
loc.segment = parts[0].to_string();
}
if parts.len() > 1 {
loc.field = parts[1].parse().unwrap_or(0);
}
if parts.len() > 2 {
loc.component = parts[2].parse().unwrap_or(0);
}
loc
});
let proto_issue = ValidationIssue {
code: issue.code,
message: issue.detail,
severity: match issue.severity {
hl7v2::Severity::Error => validation_issue::Severity::Error as i32,
hl7v2::Severity::Warning => validation_issue::Severity::Warning as i32,
},
location,
advice: String::new(),
};
if issue.severity == hl7v2::Severity::Error {
errors.push(proto_issue);
} else {
warnings.push(proto_issue);
}
}
let summary = Some(ValidationSummary {
error_count: errors.len() as i32,
warning_count: warnings.len() as i32,
info_count: 0,
});
Ok(Response::new(ValidateResponse {
valid,
errors,
warnings,
summary,
}))
}
async fn generate_ack(
&self,
request: Request<GenerateAckRequest>,
) -> Result<Response<GenerateAckResponse>, Status> {
let req = request.into_inner();
let message = rust_parse(&req.message)
.map_err(|e| Status::invalid_argument(format!("Failed to parse HL7: {}", e)))?;
let ack_code = match req.code() {
generate_ack_request::AckCode::Aa => hl7v2::AckCode::AA,
generate_ack_request::AckCode::Ae => hl7v2::AckCode::AE,
generate_ack_request::AckCode::Ar => hl7v2::AckCode::AR,
_ => hl7v2::AckCode::AA,
};
let ack_msg = hl7v2::ack(&message, ack_code)
.map_err(|e| Status::internal(format!("Failed to generate ACK: {}", e)))?;
let ack_bytes = hl7v2::write(&ack_msg);
let proto_ack = proto::Message::from(ack_msg);
Ok(Response::new(GenerateAckResponse {
ack_message: ack_bytes,
parsed_ack: Some(proto_ack),
}))
}
async fn normalize(
&self,
request: Request<NormalizeRequest>,
) -> Result<Response<NormalizeResponse>, Status> {
let req = request.into_inner();
let canonical = req
.options
.as_ref()
.map(|o| o.canonical_delimiters)
.unwrap_or(true);
let normalized_bytes = hl7v2::normalize(&req.message, canonical)
.map_err(|e| Status::invalid_argument(format!("Failed to normalize HL7: {}", e)))?;
let mut final_bytes = normalized_bytes;
if let Some(options) = req.options
&& options.mllp_frame
{
final_bytes = hl7v2::wrap_mllp(&final_bytes);
}
Ok(Response::new(NormalizeResponse {
normalized: final_bytes,
}))
}
async fn health_check(
&self,
_request: Request<HealthCheckRequest>,
) -> Result<Response<HealthCheckResponse>, Status> {
let response = HealthCheckResponse {
status: health_check_response::ServingStatus::Serving as i32,
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds: 0,
};
Ok(Response::new(response))
}
}
fn extract_grpc_metadata(msg: &RustMessage) -> MessageMetadata {
MessageMetadata {
message_type: joined_components(msg, "MSH.9").unwrap_or_else(|| "UNKNOWN".to_string()),
version: hl7v2::get(msg, "MSH.12").unwrap_or("UNKNOWN").to_string(),
control_id: hl7v2::get(msg, "MSH.10").unwrap_or("UNKNOWN").to_string(),
sending_facility: hl7v2::get(msg, "MSH.4").unwrap_or("").to_string(),
receiving_facility: hl7v2::get(msg, "MSH.6").unwrap_or("").to_string(),
}
}
fn joined_components(msg: &RustMessage, field_path: &str) -> Option<String> {
let mut components = Vec::new();
for component in 1.. {
let path = format!("{field_path}.{component}");
match hl7v2::get(msg, &path) {
Some(value) if !value.is_empty() => components.push(value.to_string()),
_ if component == 1 => return None,
_ => break,
}
}
Some(components.join("^"))
}
impl From<RustMessage> for proto::Message {
fn from(msg: RustMessage) -> Self {
proto::Message {
delimiters: Some(proto::Delimiters {
field: msg.delims.field.to_string(),
component: msg.delims.comp.to_string(),
repetition: msg.delims.rep.to_string(),
escape: msg.delims.esc.to_string(),
subcomponent: msg.delims.sub.to_string(),
}),
segments: msg.segments.into_iter().map(Into::into).collect(),
}
}
}
impl From<RustSegment> for proto::Segment {
fn from(seg: RustSegment) -> Self {
proto::Segment {
id: String::from_utf8_lossy(&seg.id).to_string(),
fields: seg.fields.into_iter().map(Into::into).collect(),
sequence: 0,
}
}
}
impl From<RustField> for proto::Field {
fn from(f: RustField) -> Self {
let presence = if f.reps.is_empty() {
proto::field::Presence::Missing as i32
} else {
proto::field::Presence::Value as i32
};
proto::Field {
presence,
value: f.first_text().map(String::from),
repetitions: f.reps.into_iter().map(Into::into).collect(),
}
}
}
impl From<RustRep> for proto::Repetition {
fn from(r: RustRep) -> Self {
proto::Repetition {
value: r.first_text().map(String::from),
components: r.comps.into_iter().map(Into::into).collect(),
}
}
}
impl From<RustComp> for proto::Component {
fn from(c: RustComp) -> Self {
proto::Component {
value: c.first_text().map(String::from),
subcomponents: c
.subs
.into_iter()
.filter_map(|a| match a {
hl7v2::Atom::Text(t) => Some(t),
_ => None,
})
.collect(),
}
}
}