Skip to main content

haste_fhir_converter/
lib.rs

1use std::{path::Path, sync::Arc};
2
3use haste_fhir_model::r4::generated::{resources::Resource, terminology::IssueType};
4use haste_fhir_operation_error::OperationOutcomeError;
5use haste_hl7v2::parser::ParsedHL7V2Message;
6use minijinja::{Environment, Template, Value};
7
8mod jinja_extensions;
9mod liquid_extensions;
10
11pub enum Input {
12    HL7V2(String),
13    FHIR(Box<Resource>),
14    JSON(serde_json::Value),
15}
16
17/// Converts an [`Input`] into a [`minijinja::Value`] suitable for template
18/// rendering.
19///
20/// HL7 v2 messages are parsed and wrapped in a Jinja-compatible object, while
21/// FHIR resources and JSON values are converted using Serde serialization.
22///
23/// # Errors
24///
25/// Returns an [`OperationOutcomeError`] if the input is an HL7 v2 message that
26/// cannot be parsed.
27pub fn convert_input(input: Input) -> Result<minijinja::Value, OperationOutcomeError> {
28    match input {
29        Input::HL7V2(message) => {
30            let parsed_message = ParsedHL7V2Message::try_from(message.as_str())?.0;
31
32            Ok(Value::from_dyn_object(Arc::new(
33                jinja_extensions::conversions::hl7v2::JHL7V2::new(parsed_message),
34            )))
35        }
36        Input::FHIR(resource) => Ok(minijinja::Value::from_serialize(resource)),
37        Input::JSON(json) => Ok(minijinja::Value::from_serialize(json)),
38    }
39}
40
41#[derive(Clone, Copy, Debug)]
42pub enum OutputFormat {
43    FHIR,
44    JSON,
45    HL7V2,
46}
47
48pub enum Output {
49    FHIR(Box<Resource>),
50    JSON(serde_json::Value),
51    HL7V2(String),
52}
53
54// Uses relative path from template directory and strips ending jinja prefix.
55fn derive_template_name(template_dir: &Path, path: &Path) -> Option<String> {
56    let relative_path = path.strip_prefix(template_dir).unwrap_or(path);
57    let Some(template_file_stem) = relative_path.file_stem().and_then(|s| s.to_str()) else {
58        eprintln!("Failed to get template name from path: {}", path.display());
59        return None;
60    };
61    let Some(parent) = relative_path.parent() else {
62        eprintln!(
63            "Failed to get parent directory for template: {}",
64            relative_path.display()
65        );
66        return None;
67    };
68
69    let template_name_path = parent.join(template_file_stem);
70
71    let Some(template_name) = template_name_path.to_str() else {
72        eprintln!(
73            "Failed to convert template name to string: {}",
74            template_name_path.display()
75        );
76        return None;
77    };
78
79    Some(template_name.to_string())
80}
81
82fn add_template(env: &mut Environment<'_>, template_dir: &Path, path: &Path) -> Option<()> {
83    let Ok(template_content) = std::fs::read_to_string(path) else {
84        eprintln!("Failed to read template file: {}", path.display());
85        return None;
86    };
87
88    let Some(template_name) = derive_template_name(template_dir, path) else {
89        eprintln!(
90            "Failed to derive template name for file: {}",
91            path.display()
92        );
93        return None;
94    };
95
96    println!(
97        "Adding template '{template_name}' from file: {}",
98        path.display()
99    );
100
101    if let Err(e) = env.add_template_owned(template_name.clone(), template_content) {
102        eprintln!("Failed to add template '{template_name}': {e}");
103    }
104
105    Some(())
106}
107
108pub fn create_environment<'a>(template_dir: Option<&str>) -> Environment<'a> {
109    let mut env = Environment::new();
110    env.add_filter(
111        "hl7v2_segments",
112        jinja_extensions::filters::hl7v2::hl7v2_segments,
113    );
114
115    if let Some(template_dir) = template_dir {
116        let template_dir = Path::new(template_dir);
117        walkdir::WalkDir::new(template_dir)
118            .into_iter()
119            .filter_map(std::result::Result::ok)
120            .filter(|e| {
121                e.file_type().is_file()
122                    && e.path()
123                        .extension()
124                        .is_some_and(|ext| ext == "jinja" || ext == "j2")
125            })
126            .for_each(|entry| {
127                let path = entry.path();
128                add_template(&mut env, template_dir, path);
129            });
130    }
131
132    env
133}
134
135/// Renders a template into the requested output format.
136///
137/// The template is rendered using the provided serializable context and the
138/// resulting text is validated or parsed according to `output`.
139///
140/// - [`OutputFormat::FHIR`] parses the rendered output as a FHIR resource.
141/// - [`OutputFormat::JSON`] parses the rendered output as arbitrary JSON.
142/// - [`OutputFormat::HL7V2`] validates that the rendered output is a valid
143///   HL7 v2 message.
144///
145/// # Errors
146///
147/// Returns an [`OperationOutcomeError`] if:
148///
149/// - template rendering fails,
150/// - the rendered output cannot be parsed as the requested format, or
151/// - HL7 v2 validation fails.
152pub fn transform<S>(
153    template: &Template<'_, '_>,
154    ctx: S,
155    output: &OutputFormat,
156) -> Result<Output, OperationOutcomeError>
157where
158    S: serde::Serialize,
159{
160    let output_data = template
161        .render(ctx)
162        .map_err(|e| OperationOutcomeError::error(IssueType::invalid(), e.to_string()))?;
163
164    match output {
165        OutputFormat::FHIR => Ok(Output::FHIR(serde_json::from_str(&output_data).map_err(
166            |e| OperationOutcomeError::error(IssueType::invalid(), e.to_string()),
167        )?)),
168        OutputFormat::JSON => Ok(Output::JSON(
169            serde_json::from_str::<serde_json::Value>(&output_data)
170                .map_err(|e| OperationOutcomeError::error(IssueType::invalid(), e.to_string()))?,
171        )),
172        OutputFormat::HL7V2 => {
173            // Verify that the output is a valid HL7v2 message by attempting to parse it.
174            ParsedHL7V2Message::try_from(output_data.as_str())?;
175
176            Ok(Output::HL7V2(output_data))
177        }
178    }
179}