haste_codegen/testscript_gen/
mod.rs1use std::path::Path;
2
3use crate::utilities::load;
4use 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}