haste-codegen 0.21.0

Code generation utilities for Haste Health FHIR server and clients.
//! Generate typescripts for testscript test cases by providing
//! `resource_files`.

use std::path::Path;

use walkdir::WalkDir;

use haste_fhir_model::r4::generated::{
    resources::{
        Resource, TestScript, TestScriptFixture, TestScriptSetupActionAssert,
        TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
        TestScriptTest, TestScriptTestAction,
    },
    terminology::{AssertDirectionCodes, BoundCode, DefinedTypes, PublicationStatus},
    types::{Coding, Meta, Reference},
};
use haste_reflect::MetaValue;

use crate::utilities::load;

fn file_path_to_resources(file_path: &Path) -> Result<Vec<Resource>, String> {
    let resource = load::load_from_file(file_path)?;

    Ok(match resource {
        Resource::Bundle(bundle) => bundle
            .entry
            .unwrap_or_default()
            .into_iter()
            .filter_map(|entry| entry.resource)
            .map(|resource| *resource)
            .collect::<Vec<_>>(),
        _ => vec![resource],
    })
}

fn get_meta_mutable(resource: &mut Resource) -> Result<&mut Meta, String> {
    let meta: &mut dyn std::any::Any = resource
        .get_field_mut("meta")
        .ok_or("Missing Meta Field".to_string())?;
    let meta: &mut Option<Box<Meta>> = meta
        .downcast_mut::<Option<Box<Meta>>>()
        .ok_or("Failed to downcast meta".to_string())?;

    if meta.is_none() {
        *meta = Some(Box::new(Meta::default()));
    }

    Ok(meta.as_mut().unwrap())
}

fn set_resource_tag(tag: &str, resource: &mut Resource) -> Result<(), String> {
    let meta = get_meta_mutable(resource)?;

    meta.tag = Some(vec![Coding {
        code: Some(Box::new(tag.to_string().into())),
        ..Default::default()
    }]);

    Ok(())
}

fn set_resource_id(id: &str, resource: &mut Resource) -> Result<(), String> {
    let id_field: &mut dyn std::any::Any = resource
        .get_field_mut("id")
        .ok_or("Missing id field".to_string())?;

    let id_field: &mut Option<String> = id_field
        .downcast_mut::<Option<String>>()
        .ok_or("Failed to downcast id field".to_string())?;

    *id_field = Some(id.to_string());

    Ok(())
}

fn fixture_name(i: usize, resource_type: &str) -> String {
    format!("fixture-{resource_type}-{i}")
}

fn generate_testcases_for_resource(
    tag: &str,
    index: usize,
    resource: &Resource,
) -> Vec<TestScriptTest> {
    let resource_type = resource.resource_type();
    let defined_type = Some(
        BoundCode::<DefinedTypes>::new(resource_type.as_ref()).expect("Unsupported resource type"),
    );

    vec![TestScriptTest {
        name: Some(Box::new(
            format!("Test for resource with tag: {tag}").into(),
        )),
        action: vec![
            TestScriptTestAction {
                operation: Some(TestScriptSetupActionOperation {
                    type_: Some(Box::new(Coding {
                        system: Some(Box::new(
                            "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
                                .to_string()
                                .into(),
                        )),
                        code: Some(Box::new("create".to_string().into())),
                        ..Default::default()
                    })),
                    resource: defined_type.clone(),
                    sourceId: Some(Box::new(
                        fixture_name(index, resource_type.as_ref()).clone().into(),
                    )),
                    responseId: Some(Box::new(fixture_name(index, resource_type.as_ref()).into())),
                    encodeRequestUrl: Box::new(true.into()),
                    ..Default::default()
                }),
                ..Default::default()
            },
            TestScriptTestAction {
                assert: Some(TestScriptSetupActionAssert {
                    label: Some(Box::new("Read created resource".to_string().into())),
                    description: Some(Box::new(
                        format!(
                            "Confirm resource of type {} created.",
                            resource_type.as_ref()
                        )
                        .into(),
                    )),
                    direction: Some(AssertDirectionCodes::response()),
                    resource: defined_type.clone(),
                    warningOnly: Box::new(false.into()),
                    ..Default::default()
                }),
                ..Default::default()
            },
        ],
        ..Default::default()
    }]
}

fn generate_fixtures_for_resource(testscript: &mut TestScript, resources: Vec<Resource>) {
    let mut contained = vec![];
    let mut fixtures = vec![];

    for (index, resource) in resources.into_iter().enumerate() {
        let resource_type = resource.resource_type();
        let fixture_id = fixture_name(index, resource_type.as_ref());

        fixtures.push(TestScriptFixture {
            id: Some(fixture_id.clone()),
            autocreate: Box::new(false.into()),
            autodelete: Box::new(false.into()),
            resource: Some(Box::new(Reference {
                reference: Some(Box::new(format!("#{fixture_id}").into())),
                ..Default::default()
            })),
            ..Default::default()
        });
        contained.push(resource);
    }

    testscript.contained = Some(contained);
    testscript.fixture = Some(fixtures);
}

fn create_tag(file_path: &Path) -> String {
    file_path.to_str().unwrap().replace(['/', '\\', '.'], "-")
}

fn generate_testscript_from_file(file_path: &Path) -> Result<TestScript, String> {
    let mut testscript = TestScript::default();
    let mut resources = file_path_to_resources(file_path)?;

    let tag = create_tag(file_path);

    testscript.url = Box::new(tag.clone().into());
    testscript.status = PublicationStatus::active();
    testscript.id = Some(tag.clone());
    testscript.name = Box::new(tag.clone().into());

    for (i, resource) in resources.iter_mut().enumerate() {
        set_resource_tag(&tag, resource).expect("Failed to set resource tag");
        set_resource_id(
            &fixture_name(i, resource.resource_type().as_ref()),
            resource,
        )
        .expect("Failed to set resource id");
    }

    generate_fixtures_for_resource(&mut testscript, resources.clone());

    testscript.test = Some(
        resources
            .iter()
            .enumerate()
            .flat_map(|(i, r)| generate_testcases_for_resource(&tag, i, r))
            .collect::<Vec<_>>(),
    );

    testscript.teardown = Some(TestScriptTeardown {
        action: vec![TestScriptTeardownAction {
            operation: TestScriptSetupActionOperation {
                type_: Some(Box::new(Coding {
                    system: Some(Box::new(
                        "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
                            .to_string()
                            .into(),
                    )),
                    code: Some(Box::new("deleteCondMultiple".to_string().into())),
                    ..Default::default()
                })),
                encodeRequestUrl: Box::new(true.into()),
                resource: None,
                params: Some(Box::new(format!("_tag={tag}").into())),
                description: Some(Box::new(
                    "Delete resources created in test.".to_string().into(),
                )),

                ..Default::default()
            },
            ..Default::default()
        }],
        ..Default::default()
    });

    Ok(testscript)
}

/// Generates test scripts from the provided file paths.
///
/// # Errors
///
/// Returns an error if a test script cannot be generated from a file.
pub fn generate_testscripts(file_paths: &[String]) -> Result<Vec<TestScript>, String> {
    let mut testscripts = Vec::new();

    for dir_path in file_paths {
        let walker = WalkDir::new(dir_path).sort_by_file_name().into_iter();
        for entry in walker
            .filter_map(std::result::Result::ok)
            .filter(|e| e.metadata().is_ok_and(|metadata| metadata.is_file()))
        {
            let testscript = generate_testscript_from_file(entry.path())?;
            testscripts.push(testscript);
        }
    }

    Ok(testscripts)
}