Skip to main content

haste_codegen/testscript_gen/
mod.rs

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