haste_codegen/testscript_gen/
mod.rs1use 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
226pub 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}