Skip to main content

haste_codegen/testscript_gen/
mod.rs

1//! Generate typescripts for testscript test cases by providing
2//! `resource_files`.
3
4use std::path::Path;
5
6use walkdir::WalkDir;
7
8use haste_fhir_model::r4::generated::{
9    resources::{
10        Resource, TestScript, TestScriptFixture, TestScriptSetupActionAssert,
11        TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
12        TestScriptTest, TestScriptTestAction,
13    },
14    terminology::{AssertDirectionCodes, BoundCode, DefinedTypes, PublicationStatus},
15    types::{Coding, Meta, Reference},
16};
17use haste_reflect::MetaValue;
18
19use crate::utilities::load;
20
21fn file_path_to_resources(file_path: &Path) -> Result<Vec<Resource>, String> {
22    let resource = load::load_from_file(file_path)?;
23
24    Ok(match resource {
25        Resource::Bundle(bundle) => bundle
26            .entry
27            .unwrap_or_default()
28            .into_iter()
29            .filter_map(|entry| entry.resource)
30            .map(|resource| *resource)
31            .collect::<Vec<_>>(),
32        _ => vec![resource],
33    })
34}
35
36fn get_meta_mutable(resource: &mut Resource) -> Result<&mut Meta, String> {
37    let meta: &mut dyn std::any::Any = resource
38        .get_field_mut("meta")
39        .ok_or("Missing Meta Field".to_string())?;
40    let meta: &mut Option<Box<Meta>> = meta
41        .downcast_mut::<Option<Box<Meta>>>()
42        .ok_or("Failed to downcast meta".to_string())?;
43
44    if meta.is_none() {
45        *meta = Some(Box::new(Meta::default()));
46    }
47
48    Ok(meta.as_mut().unwrap())
49}
50
51fn set_resource_tag(tag: &str, resource: &mut Resource) -> Result<(), String> {
52    let meta = get_meta_mutable(resource)?;
53
54    meta.tag = Some(vec![Coding {
55        code: Some(Box::new(tag.to_string().into())),
56        ..Default::default()
57    }]);
58
59    Ok(())
60}
61
62fn set_resource_id(id: &str, resource: &mut Resource) -> Result<(), String> {
63    let id_field: &mut dyn std::any::Any = resource
64        .get_field_mut("id")
65        .ok_or("Missing id field".to_string())?;
66
67    let id_field: &mut Option<String> = id_field
68        .downcast_mut::<Option<String>>()
69        .ok_or("Failed to downcast id field".to_string())?;
70
71    *id_field = Some(id.to_string());
72
73    Ok(())
74}
75
76fn fixture_name(i: usize, resource_type: &str) -> String {
77    format!("fixture-{resource_type}-{i}")
78}
79
80fn generate_testcases_for_resource(
81    tag: &str,
82    index: usize,
83    resource: &Resource,
84) -> Vec<TestScriptTest> {
85    let resource_type = resource.resource_type();
86    let defined_type = Some(
87        BoundCode::<DefinedTypes>::new(resource_type.as_ref()).expect("Unsupported resource type"),
88    );
89
90    vec![TestScriptTest {
91        name: Some(Box::new(
92            format!("Test for resource with tag: {tag}").into(),
93        )),
94        action: vec![
95            TestScriptTestAction {
96                operation: Some(TestScriptSetupActionOperation {
97                    type_: Some(Box::new(Coding {
98                        system: Some(Box::new(
99                            "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
100                                .to_string()
101                                .into(),
102                        )),
103                        code: Some(Box::new("create".to_string().into())),
104                        ..Default::default()
105                    })),
106                    resource: defined_type.clone(),
107                    sourceId: Some(Box::new(
108                        fixture_name(index, resource_type.as_ref()).clone().into(),
109                    )),
110                    responseId: Some(Box::new(fixture_name(index, resource_type.as_ref()).into())),
111                    encodeRequestUrl: Box::new(true.into()),
112                    ..Default::default()
113                }),
114                ..Default::default()
115            },
116            TestScriptTestAction {
117                assert: Some(TestScriptSetupActionAssert {
118                    label: Some(Box::new("Read created resource".to_string().into())),
119                    description: Some(Box::new(
120                        format!(
121                            "Confirm resource of type {} created.",
122                            resource_type.as_ref()
123                        )
124                        .into(),
125                    )),
126                    direction: Some(AssertDirectionCodes::response()),
127                    resource: defined_type.clone(),
128                    warningOnly: Box::new(false.into()),
129                    ..Default::default()
130                }),
131                ..Default::default()
132            },
133        ],
134        ..Default::default()
135    }]
136}
137
138fn generate_fixtures_for_resource(testscript: &mut TestScript, resources: Vec<Resource>) {
139    let mut contained = vec![];
140    let mut fixtures = vec![];
141
142    for (index, resource) in resources.into_iter().enumerate() {
143        let resource_type = resource.resource_type();
144        let fixture_id = fixture_name(index, resource_type.as_ref());
145
146        fixtures.push(TestScriptFixture {
147            id: Some(fixture_id.clone()),
148            autocreate: Box::new(false.into()),
149            autodelete: Box::new(false.into()),
150            resource: Some(Box::new(Reference {
151                reference: Some(Box::new(format!("#{fixture_id}").into())),
152                ..Default::default()
153            })),
154            ..Default::default()
155        });
156        contained.push(resource);
157    }
158
159    testscript.contained = Some(contained);
160    testscript.fixture = Some(fixtures);
161}
162
163fn create_tag(file_path: &Path) -> String {
164    file_path.to_str().unwrap().replace(['/', '\\', '.'], "-")
165}
166
167fn generate_testscript_from_file(file_path: &Path) -> Result<TestScript, String> {
168    let mut testscript = TestScript::default();
169    let mut resources = file_path_to_resources(file_path)?;
170
171    let tag = create_tag(file_path);
172
173    testscript.url = Box::new(tag.clone().into());
174    testscript.status = PublicationStatus::active();
175    testscript.id = Some(tag.clone());
176    testscript.name = Box::new(tag.clone().into());
177
178    for (i, resource) in resources.iter_mut().enumerate() {
179        set_resource_tag(&tag, resource).expect("Failed to set resource tag");
180        set_resource_id(
181            &fixture_name(i, resource.resource_type().as_ref()),
182            resource,
183        )
184        .expect("Failed to set resource id");
185    }
186
187    generate_fixtures_for_resource(&mut testscript, resources.clone());
188
189    testscript.test = Some(
190        resources
191            .iter()
192            .enumerate()
193            .flat_map(|(i, r)| generate_testcases_for_resource(&tag, i, r))
194            .collect::<Vec<_>>(),
195    );
196
197    testscript.teardown = Some(TestScriptTeardown {
198        action: vec![TestScriptTeardownAction {
199            operation: TestScriptSetupActionOperation {
200                type_: Some(Box::new(Coding {
201                    system: Some(Box::new(
202                        "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
203                            .to_string()
204                            .into(),
205                    )),
206                    code: Some(Box::new("deleteCondMultiple".to_string().into())),
207                    ..Default::default()
208                })),
209                encodeRequestUrl: Box::new(true.into()),
210                resource: None,
211                params: Some(Box::new(format!("_tag={tag}").into())),
212                description: Some(Box::new(
213                    "Delete resources created in test.".to_string().into(),
214                )),
215
216                ..Default::default()
217            },
218            ..Default::default()
219        }],
220        ..Default::default()
221    });
222
223    Ok(testscript)
224}
225
226/// Generates test scripts from the provided file paths.
227///
228/// # Errors
229///
230/// Returns an error if a test script cannot be generated from a file.
231pub fn generate_testscripts(file_paths: &[String]) -> Result<Vec<TestScript>, String> {
232    let mut testscripts = Vec::new();
233
234    for dir_path in file_paths {
235        let walker = WalkDir::new(dir_path).sort_by_file_name().into_iter();
236        for entry in walker
237            .filter_map(std::result::Result::ok)
238            .filter(|e| e.metadata().is_ok_and(|metadata| metadata.is_file()))
239        {
240            let testscript = generate_testscript_from_file(entry.path())?;
241            testscripts.push(testscript);
242        }
243    }
244
245    Ok(testscripts)
246}