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(Resource),
14    JSON(serde_json::Value),
15}
16
17pub fn convert_input(input: Input) -> Result<minijinja::Value, OperationOutcomeError> {
18    match input {
19        Input::HL7V2(message) => {
20            let parsed_message = ParsedHL7V2Message::try_from(message.as_str())?.0;
21
22            Ok(Value::from_dyn_object(Arc::new(
23                jinja_extensions::conversions::hl7v2::JHL7V2::new(parsed_message),
24            )))
25        }
26        Input::FHIR(resource) => Ok(minijinja::Value::from_serialize(resource)),
27        Input::JSON(json) => Ok(minijinja::Value::from_serialize(json)),
28    }
29}
30
31#[derive(Clone, Copy, Debug)]
32pub enum OutputFormat {
33    FHIR,
34    JSON,
35    HL7V2,
36}
37
38pub enum Output {
39    FHIR(Resource),
40    JSON(serde_json::Value),
41    HL7V2(String),
42}
43
44// Uses relative path from template directory and strips ending jinja prefix.
45fn derive_template_name(template_dir: &Path, path: &Path) -> Option<String> {
46    let relative_path = path.strip_prefix(template_dir).unwrap_or(path);
47    let Some(template_file_stem) = relative_path.file_stem().and_then(|s| s.to_str()) else {
48        eprintln!("Failed to get template name from path: {:?}", path);
49        return None;
50    };
51    let Some(parent) = relative_path.parent() else {
52        eprintln!(
53            "Failed to get parent directory for template: {:?}",
54            relative_path
55        );
56        return None;
57    };
58
59    let template_name_path = parent.join(template_file_stem);
60
61    let Some(template_name) = template_name_path.to_str() else {
62        eprintln!(
63            "Failed to convert template name to string: {:?}",
64            template_name_path
65        );
66        return None;
67    };
68
69    Some(template_name.to_string())
70}
71
72fn add_template(env: &mut Environment<'_>, template_dir: &Path, path: &Path) -> Option<()> {
73    let Ok(template_content) = std::fs::read_to_string(path) else {
74        eprintln!("Failed to read template file: {:?}", path);
75        return None;
76    };
77
78    let Some(template_name) = derive_template_name(template_dir, path) else {
79        eprintln!("Failed to derive template name for file: {:?}", path);
80        return None;
81    };
82
83    println!("Adding template '{}' from file: {:?}", template_name, path);
84
85    if let Err(e) = env.add_template_owned(template_name.to_string(), template_content) {
86        eprintln!("Failed to add template '{}': {}", template_name, e);
87    }
88
89    Some(())
90}
91
92pub fn create_environment<'a>(template_dir: Option<&str>) -> Environment<'a> {
93    let mut env = Environment::new();
94    env.add_filter(
95        "hl7v2_segments",
96        jinja_extensions::filters::hl7v2::hl7v2_segments,
97    );
98
99    if let Some(template_dir) = template_dir {
100        let template_dir = Path::new(template_dir);
101        walkdir::WalkDir::new(template_dir)
102            .into_iter()
103            .filter_map(|e| e.ok())
104            .filter(|e| {
105                e.file_type().is_file()
106                    && e.path()
107                        .extension()
108                        .map_or(false, |ext| ext == "jinja" || ext == "j2")
109            })
110            .for_each(|entry| {
111                let path = entry.path();
112                add_template(&mut env, template_dir, path);
113            });
114    }
115
116    env
117}
118
119pub fn transform<S>(
120    template: &Template<'_, '_>,
121    ctx: S,
122    output: &OutputFormat,
123) -> Result<Output, OperationOutcomeError>
124where
125    S: serde::Serialize,
126{
127    let output_data = template
128        .render(ctx)
129        .map_err(|e| OperationOutcomeError::error(IssueType::INVALID, e.to_string()))?;
130
131    match output {
132        OutputFormat::FHIR => Ok(Output::FHIR(
133            serde_json::from_str(&output_data)
134                .map_err(|e| OperationOutcomeError::error(IssueType::INVALID, e.to_string()))?,
135        )),
136        OutputFormat::JSON => Ok(Output::JSON(
137            serde_json::from_str::<serde_json::Value>(&output_data)
138                .map_err(|e| OperationOutcomeError::error(IssueType::INVALID, e.to_string()))?,
139        )),
140        OutputFormat::HL7V2 => {
141            // Verify that the output is a valid HL7v2 message by attempting to parse it.
142            ParsedHL7V2Message::try_from(output_data.as_str())?.0;
143
144            Ok(Output::HL7V2(output_data))
145        }
146    }
147}