hl7v2-server 1.3.0

HTTP/REST API server for HL7v2 message processing
//! gRPC service implementation for HL7v2.

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};

// Include the generated gRPC code
/// Generated gRPC protocol code (protobuf messages and service traits).
#[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::*;

/// Implementation of the HL7Service gRPC trait
pub struct Hl7ServiceImpl {
    #[expect(
        dead_code,
        reason = "service state is retained for runtime parity and future streaming support"
    )]
    state: Arc<AppState>,
}

impl Hl7ServiceImpl {
    /// Create a new gRPC service instance
    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))
    }
}

// ============================================================================
// Conversions
// ============================================================================

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(),
        }
    }
}