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, 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(tag.to_string().into())),
51        ..Default::default()
52    })]);
53
54    Ok(())
55}
56
57fn set_resource_id(id: &str, resource: &mut Resource) -> Result<(), String> {
58    let id_field: &mut dyn std::any::Any = resource
59        .get_field_mut("id")
60        .ok_or("Missing id field".to_string())?;
61
62    let id_field: &mut Option<String> = id_field
63        .downcast_mut::<Option<String>>()
64        .ok_or("Failed to downcast id field".to_string())?;
65
66    *id_field = Some(id.to_string());
67
68    Ok(())
69}
70
71fn fixture_name(i: usize, resource_type: &str) -> String {
72    format!("fixture-{}-{}", resource_type, i)
73}
74
75fn generate_testcases_for_resource(
76    tag: &str,
77    index: usize,
78    resource: &Resource,
79) -> Vec<TestScriptTest> {
80    let resource_type = resource.resource_type();
81    let defined_type = Some(Box::new(
82        DefinedTypes::try_from(resource_type.as_ref().to_string())
83            .expect("Unsupported resource type"),
84    ));
85
86    vec![TestScriptTest {
87        name: Some(Box::new(
88            format!("Test for resource with tag: {}", tag).into(),
89        )),
90        action: vec![
91            TestScriptTestAction {
92                operation: Some(TestScriptSetupActionOperation {
93                    type_: Some(Box::new(Coding {
94                        system: Some(Box::new(
95                            "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
96                                .to_string()
97                                .into(),
98                        )),
99                        code: Some(Box::new("create".to_string().into())),
100                        ..Default::default()
101                    })),
102                    resource: defined_type.clone(),
103                    sourceId: Some(Box::new(
104                        fixture_name(index, resource_type.as_ref())
105                            .to_string()
106                            .into(),
107                    )),
108                    responseId: Some(Box::new(fixture_name(index, resource_type.as_ref()).into())),
109                    encodeRequestUrl: Box::new(true.into()),
110                    ..Default::default()
111                }),
112                ..Default::default()
113            },
114            TestScriptTestAction {
115                assert: Some(TestScriptSetupActionAssert {
116                    label: Some(Box::new("Read created resource".to_string().into())),
117                    description: Some(Box::new(
118                        format!(
119                            "Confirm resource of type {} created.",
120                            resource_type.as_ref()
121                        )
122                        .into(),
123                    )),
124                    direction: Some(Box::new(AssertDirectionCodes::Response(None))),
125                    resource: defined_type.clone(),
126                    warningOnly: Box::new(false.into()),
127                    ..Default::default()
128                }),
129                ..Default::default()
130            },
131        ],
132        ..Default::default()
133    }]
134}
135
136fn generate_fixtures_for_resource(
137    testscript: &mut TestScript,
138    resources: Vec<Box<Resource>>,
139) -> Result<(), String> {
140    let mut contained = vec![];
141    let mut fixtures = vec![];
142
143    for (index, resource) in resources.into_iter().enumerate() {
144        let resource_type = resource.resource_type();
145        let fixture_id = fixture_name(index, resource_type.as_ref());
146
147        fixtures.push(TestScriptFixture {
148            id: Some(fixture_id.clone()),
149            autocreate: Box::new(false.into()),
150            autodelete: Box::new(false.into()),
151            resource: Some(Box::new(Reference {
152                reference: Some(Box::new(format!("#{}", fixture_id).into())),
153                ..Default::default()
154            })),
155            ..Default::default()
156        });
157        contained.push(resource);
158    }
159
160    testscript.contained = Some(contained);
161    testscript.fixture = Some(fixtures);
162
163    Ok(())
164}
165
166fn create_tag(file_path: &Path) -> String {
167    file_path
168        .to_str()
169        .unwrap()
170        .replace("/", "-")
171        .replace("\\", "-")
172        .replace(".", "-")
173}
174
175fn generate_testscript_from_file(file_path: &Path) -> Result<TestScript, String> {
176    let mut testscript = TestScript::default();
177    let mut resources = file_path_to_resources(file_path)?;
178
179    let tag = create_tag(file_path);
180
181    testscript.url = Box::new(tag.to_string().into());
182    testscript.status = Box::new(PublicationStatus::Active(None));
183    testscript.id = Some(tag.to_string());
184    testscript.name = Box::new(tag.to_string().into());
185
186    for (i, resource) in resources.iter_mut().enumerate() {
187        set_resource_tag(&tag, resource).expect("Failed to set resource tag");
188        set_resource_id(
189            &fixture_name(i, &resource.resource_type().as_ref()),
190            resource,
191        )
192        .expect("Failed to set resource id");
193    }
194
195    generate_fixtures_for_resource(&mut testscript, resources.clone())?;
196
197    testscript.test = Some(
198        resources
199            .iter()
200            .enumerate()
201            .map(|(i, r)| generate_testcases_for_resource(&tag, i, r))
202            .flatten()
203            .collect::<Vec<_>>(),
204    );
205
206    testscript.teardown = Some(TestScriptTeardown {
207        action: vec![TestScriptTeardownAction {
208            operation: TestScriptSetupActionOperation {
209                type_: Some(Box::new(Coding {
210                    system: Some(Box::new(
211                        "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
212                            .to_string()
213                            .into(),
214                    )),
215                    code: Some(Box::new("deleteCondMultiple".to_string().into())),
216                    ..Default::default()
217                })),
218                encodeRequestUrl: Box::new(true.into()),
219                resource: None,
220                params: Some(Box::new(format!("_tag={}", tag).into())),
221                description: Some(Box::new(
222                    "Delete resources created in test.".to_string().into(),
223                )),
224
225                ..Default::default()
226            },
227            ..Default::default()
228        }],
229        ..Default::default()
230    });
231
232    Ok(testscript)
233}
234
235pub fn generate_testscripts(file_paths: &Vec<String>) -> Result<Vec<TestScript>, String> {
236    let mut testscripts = vec![];
237    for dir_path in file_paths {
238        let walker = WalkDir::new(dir_path).into_iter();
239        for entry in walker
240            .filter_map(|e| e.ok())
241            .filter(|e| e.metadata().unwrap().is_file())
242        {
243            let testscript = generate_testscript_from_file(&entry.path().to_path_buf())?;
244            testscripts.push(testscript);
245        }
246    }
247
248    Ok(testscripts)
249}