1use haste_fhir_client::{
2 FHIRClient,
3 request::{
4 DeleteRequest, FHIRCreateRequest, FHIRDeleteInstanceRequest, FHIRDeleteSystemRequest,
5 FHIRDeleteTypeRequest, FHIRHistoryInstanceRequest, FHIRHistorySystemRequest,
6 FHIRHistoryTypeRequest, FHIRInvokeInstanceRequest, FHIRInvokeSystemRequest,
7 FHIRInvokeTypeRequest, FHIRReadRequest, FHIRRequest, FHIRResponse, FHIRTransactionRequest,
8 FHIRUpdateInstanceRequest, FHIRVersionReadRequest, HistoryRequest, HistoryResponse,
9 InvocationRequest, InvokeResponse, Operation, SearchResponse, UpdateRequest,
10 },
11 url::ParsedParameters,
12};
13use haste_fhir_model::r4::generated::{
14 resources::{
15 Resource, ResourceType, TestReport, TestReportSetup, TestReportSetupAction,
16 TestReportSetupActionAssert, TestReportSetupActionOperation, TestReportTeardown,
17 TestReportTeardownAction, TestReportTest, TestReportTestAction, TestScript,
18 TestScriptFixture, TestScriptSetup, TestScriptSetupAction, TestScriptSetupActionAssert,
19 TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
20 TestScriptTest, TestScriptTestAction, TestScriptVariable,
21 },
22 terminology::{
23 AssertDirectionCodes, AssertOperatorCodes, BundleType, IssueType, ReportActionResultCodes,
24 ReportResultCodes, ReportStatusCodes, TestscriptOperationCodes,
25 },
26 types::{FHIRId, FHIRMarkdown, FHIRString, Reference},
27};
28use haste_fhir_operation_error::OperationOutcomeError;
29use haste_pointer::{Key, TypedPointer};
30use haste_reflect::MetaValue;
31use regex::Regex;
32use std::{
33 any::Any,
34 collections::HashMap,
35 sync::{Arc, LazyLock},
36 time::Duration,
37};
38use tokio::sync::Mutex;
39
40use crate::conversion::ConvertedValue;
41
42mod conversion;
43
44#[derive(Debug)]
45pub enum TestScriptError {
46 ExecutionError(String),
47 ValidationError(String),
48 FixtureNotFound,
49 InvalidFixture,
50 OperationError(OperationOutcomeError),
51}
52
53#[derive(Debug, Clone)]
54enum Response {
55 FHIRResponse(FHIRResponse),
56 OperationError(Arc<OperationOutcomeError>),
57}
58
59#[derive(Debug)]
60enum Fixtures {
61 Resource(Resource),
62 Request(FHIRRequest),
63 Response(Response),
64}
65
66struct TestState {
68 fp_engine: haste_fhirpath::FPEngine,
69 fixtures: HashMap<String, Fixtures>,
70 latest_request: Option<FHIRRequest>,
71 latest_response: Option<Response>,
72 result: ReportResultCodes,
73}
74
75impl TestState {
76 fn new() -> Self {
77 TestState {
78 fp_engine: haste_fhirpath::FPEngine::new(),
79 fixtures: HashMap::new(),
80 latest_request: None,
81 latest_response: None,
82 result: ReportResultCodes::Pending(None),
83 }
84 }
85 fn resolve_fixture<'a>(
86 &'a self,
87 fixture_id: &str,
88 ) -> Result<&'a dyn MetaValue, TestScriptError> {
89 let fixture = self
90 .fixtures
91 .get(fixture_id)
92 .ok_or(TestScriptError::FixtureNotFound)?;
93
94 match fixture {
95 Fixtures::Resource(res) => Ok(res),
96 Fixtures::Request(req) => {
97 request_to_meta_value(req).ok_or_else(|| TestScriptError::InvalidFixture)
98 }
99 Fixtures::Response(response) => {
100 response_to_meta_value(response).ok_or_else(|| TestScriptError::InvalidFixture)
101 }
102 }
103 }
104}
105
106struct TestResult<T> {
107 pub state: Arc<Mutex<TestState>>,
108 pub value: T,
109}
110
111fn response_to_meta_value<'a>(response: &'a Response) -> Option<&'a dyn MetaValue> {
112 match response {
113 Response::FHIRResponse(fhir_response) => match fhir_response {
114 FHIRResponse::Create(res) => Some(&res.resource),
115 FHIRResponse::Read(res) => Some(&res.resource),
116 FHIRResponse::VersionRead(res) => Some(&res.resource),
117 FHIRResponse::Update(res) => Some(&res.resource),
118 FHIRResponse::Patch(res) => Some(&res.resource),
119 FHIRResponse::Batch(res) => Some(&res.resource),
120 FHIRResponse::Transaction(res) => Some(&res.resource),
121
122 FHIRResponse::Capabilities(res) => Some(&res.capabilities),
123 FHIRResponse::Search(res) => match res {
124 SearchResponse::Type(res) => Some(&res.bundle),
125 SearchResponse::System(res) => Some(&res.bundle),
126 },
127 FHIRResponse::History(res) => match res {
128 HistoryResponse::Instance(res) => Some(&res.bundle),
129 HistoryResponse::Type(res) => Some(&res.bundle),
130 HistoryResponse::System(res) => Some(&res.bundle),
131 },
132 FHIRResponse::Invoke(res) => match res {
133 InvokeResponse::Instance(res) => Some(&res.resource),
134 InvokeResponse::Type(res) => Some(&res.resource),
135 InvokeResponse::System(res) => Some(&res.resource),
136 },
137
138 FHIRResponse::Delete(_) => None,
139 },
140 Response::OperationError(op_error) => {
141 let outcome = op_error.outcome();
142 Some(outcome)
143 }
144 }
145}
146
147fn request_to_meta_value<'a>(request: &'a FHIRRequest) -> Option<&'a dyn MetaValue> {
148 match request {
149 FHIRRequest::Create(req) => Some(&req.resource),
150
151 FHIRRequest::Update(update_request) => match update_request {
152 UpdateRequest::Conditional(req) => Some(&req.resource),
153 UpdateRequest::Instance(req) => Some(&req.resource),
154 },
155
156 FHIRRequest::Batch(req) => Some(&req.resource),
157 FHIRRequest::Transaction(req) => Some(&req.resource),
158 FHIRRequest::Invocation(req) => match req {
159 haste_fhir_client::request::InvocationRequest::Instance(req) => Some(&req.parameters),
160 haste_fhir_client::request::InvocationRequest::Type(req) => Some(&req.parameters),
161 haste_fhir_client::request::InvocationRequest::System(req) => Some(&req.parameters),
162 },
163 FHIRRequest::Read(_)
164 | FHIRRequest::VersionRead(_)
165 | FHIRRequest::Compartment(_)
166 | FHIRRequest::Patch(_)
167 | FHIRRequest::Delete(_)
168 | FHIRRequest::Capabilities
169 | FHIRRequest::Search(_)
170 | FHIRRequest::History(_) => None,
171 }
172}
173
174fn associate_request_response_variables(
175 state: &mut TestState,
176 operation: &TestScriptSetupActionOperation,
177 request: FHIRRequest,
178 response: Response,
179) {
180 if let Some(request_var) = operation
181 .requestId
182 .as_ref()
183 .and_then(|id| id.value.as_ref())
184 {
185 state
187 .fixtures
188 .insert(request_var.clone(), Fixtures::Request(request.clone()));
189 }
190
191 if let Some(response_var) = operation
192 .responseId
193 .as_ref()
194 .and_then(|id| id.value.as_ref())
195 {
196 state
198 .fixtures
199 .insert(response_var.clone(), Fixtures::Response(response.clone()));
200 }
201
202 state.latest_request = Some(request);
203 state.latest_response = Some(response);
204}
205
206fn derive_resource_type(
208 operation: &TestScriptSetupActionOperation,
209 target: Option<&dyn MetaValue>,
210 path: &str,
211) -> Result<ResourceType, TestScriptError> {
212 if let Some(operation_resource_type) = operation.resource.as_ref() {
213 let string_type: Option<String> = operation_resource_type.as_ref().into();
214 ResourceType::try_from(string_type.unwrap_or_default()).map_err(|_| {
215 TestScriptError::ExecutionError(format!(
216 "Unsupported resource type '{:?}' for operation at '{}'.",
217 operation_resource_type.as_ref(),
218 path
219 ))
220 })
221 } else if let Some(target) = target {
222 ResourceType::try_from(target.typename()).map_err(|_| {
223 TestScriptError::ExecutionError(format!(
224 "Unsupported resource type '{}' for operation at '{}'.",
225 target.typename(),
226 path
227 ))
228 })
229 } else {
230 Err(TestScriptError::ExecutionError(format!(
231 "Failed to derive resource type for operation at '{}'.",
232 path
233 )))
234 }
235}
236
237static EXPRESSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]*)\}").unwrap());
238
239async fn get_variable(
240 state: &TestState,
241 variables: &Vec<TestScriptVariable>,
242 variable_id: &str,
243) -> Result<ConvertedValue, TestScriptError> {
244 let Some(variable) = variables
245 .iter()
246 .find(|v| v.name.value.as_ref().map(|s| s.as_str()) == Some(variable_id))
247 else {
248 return Err(TestScriptError::ExecutionError(format!(
249 "Variable with id '{}' not found.",
250 variable_id
251 )));
252 };
253
254 if let Some(expression) = variable
255 .expression
256 .as_ref()
257 .and_then(|exp| exp.value.as_ref())
258 {
259 let values =
260 if let Some(source_id) = variable.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
261 let source = state.resolve_fixture(source_id)?;
262 vec![source]
263 } else {
264 vec![]
265 };
266
267 let eval_result = state
268 .fp_engine
269 .evaluate(expression, values)
270 .await
271 .map_err(|e| {
272 TestScriptError::ExecutionError(format!(
273 "Failed to evaluate FHIRPath expression for variable '{}': {}",
274 variable_id, e
275 ))
276 })?;
277
278 let converted_values = eval_result
279 .iter()
280 .map(|d| {
281 conversion::convert_meta_value(d).ok_or_else(|| {
282 TestScriptError::ExecutionError(format!(
283 "Failed to convert comparison fixture value '{}'.",
284 d.typename()
285 ))
286 })
287 })
288 .collect::<Result<Vec<_>, TestScriptError>>()?;
289
290 if converted_values.len() == 1 {
291 Ok(converted_values.into_iter().next().unwrap())
292 } else {
293 Err(TestScriptError::ExecutionError(format!(
294 "Variable '{}' evaluation returned multiple values; only single value supported.",
295 variable_id
296 )))
297 }
298 } else {
299 return Err(TestScriptError::ExecutionError(format!(
300 "Only support variable with expression for variable id '{}'.",
301 variable_id
302 )));
303 }
304}
305
306async fn evaluate_variable(
307 state: &TestState,
308 pointer: TypedPointer<TestScript, TestScript>,
309 value: &str,
310) -> Result<String, TestScriptError> {
311 let mut result = value.to_string();
312 let variable_pointer =
313 pointer.descend::<Vec<TestScriptVariable>>(&Key::Field("variable".to_string()));
314 let default_variables = vec![];
315
316 let variables = if let Some(pointer) = variable_pointer.as_ref() {
317 pointer.value().unwrap_or(&default_variables)
318 } else {
319 &default_variables
320 };
321
322 for reg_match in EXPRESSION_REGEX.captures_iter(value) {
323 let full_match = reg_match.get(0).map(|m| m.as_str()).unwrap_or("");
324 let Some(variable_id) = reg_match.get(1).map(|m| m.as_str()) else {
325 return Err(TestScriptError::ExecutionError(format!(
326 "Invalid variable expression in '{}'.",
327 value
328 )));
329 };
330
331 let variable = get_variable(state, variables, variable_id).await?;
332 result = result.replace(full_match, variable.to_string().as_str());
333 }
334
335 Ok(result)
336}
337
338async fn testscript_operation_to_fhir_request(
339 state: &TestState,
340 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
341) -> Result<FHIRRequest, TestScriptError> {
342 let operation = pointer.value().ok_or_else(|| {
343 TestScriptError::ExecutionError(format!(
344 "Failed to retrieve TestScript operation at '{}'.",
345 pointer.path()
346 ))
347 })?;
348
349 let operation_type = operation
350 .type_
351 .as_ref()
352 .and_then(|t| t.code.as_ref())
353 .and_then(|c| c.value.clone());
354
355 if operation_type == (&TestscriptOperationCodes::Read(None)).into() {
356 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
357 return Err(TestScriptError::ExecutionError(format!(
358 "Read operation requires targetId at '{}'.",
359 pointer.path()
360 )));
361 };
362
363 let target = state.resolve_fixture(target_id)?;
364
365 Ok(FHIRRequest::Read(FHIRReadRequest {
366 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
367 id: target
368 .get_field("id")
369 .ok_or_else(|| {
370 TestScriptError::ExecutionError(format!(
371 "Target fixture '{}' does not have an 'id' field.",
372 target_id
373 ))
374 })?
375 .as_any()
376 .downcast_ref::<String>()
377 .cloned()
378 .unwrap_or_default(),
379 }))
380 } else if operation_type == (&TestscriptOperationCodes::Vread(None)).into() {
381 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
382 return Err(TestScriptError::ExecutionError(format!(
383 "Version Read operation requires targetId at '{}'.",
384 pointer.path()
385 )));
386 };
387 let target = state.resolve_fixture(target_id)?;
388
389 let id = target
390 .get_field("id")
391 .ok_or_else(|| {
392 TestScriptError::ExecutionError(format!(
393 "Target fixture '{}' does not have an 'id' field.",
394 target_id
395 ))
396 })?
397 .as_any()
398 .downcast_ref::<String>()
399 .cloned()
400 .unwrap_or_default();
401 let version_id = target
402 .get_field("meta")
403 .and_then(|meta| meta.get_field("versionId"))
404 .ok_or_else(|| {
405 TestScriptError::ExecutionError(format!(
406 "Target fixture '{}' does not have an 'versionId' field.",
407 target_id
408 ))
409 })?
410 .as_any()
411 .downcast_ref::<Box<FHIRId>>()
412 .cloned()
413 .and_then(|v| v.value)
414 .ok_or_else(|| {
415 TestScriptError::ExecutionError(format!(
416 "Target fixture '{}' does not have an 'versionId' field.",
417 target_id
418 ))
419 })?;
420
421 Ok(FHIRRequest::VersionRead(FHIRVersionReadRequest {
422 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
423 id: id,
424 version_id: version_id.into(),
425 }))
426 } else if operation_type == (&TestscriptOperationCodes::Search(None)).into() {
427 let query_string = operation
428 .params
429 .as_ref()
430 .and_then(|p| p.value.as_ref())
431 .cloned()
432 .unwrap_or_default();
433
434 let processed_query = ParsedParameters::try_from(
435 evaluate_variable(state, pointer.root(), &query_string)
436 .await?
437 .as_str(),
438 )
439 .map_err(|e| {
440 TestScriptError::ExecutionError(format!(
441 "Failed to parse parameters for History operation at '{}': {}",
442 pointer.path(),
443 e
444 ))
445 })?;
446
447 if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
448 Ok(FHIRRequest::Search(
449 haste_fhir_client::request::SearchRequest::Type(
450 haste_fhir_client::request::FHIRSearchTypeRequest {
451 resource_type,
452 parameters: processed_query,
453 },
454 ),
455 ))
456 } else {
457 Ok(FHIRRequest::Search(
458 haste_fhir_client::request::SearchRequest::System(
459 haste_fhir_client::request::FHIRSearchSystemRequest {
460 parameters: processed_query,
461 },
462 ),
463 ))
464 }
465 } else if operation_type == (&TestscriptOperationCodes::History(None)).into() {
466 let query_string = operation
467 .params
468 .as_ref()
469 .and_then(|p| p.value.as_ref())
470 .cloned()
471 .unwrap_or_default();
472
473 let processed_query = ParsedParameters::try_from(
474 evaluate_variable(state, pointer.root(), &query_string)
475 .await?
476 .as_str(),
477 )
478 .map_err(|e| {
479 TestScriptError::ExecutionError(format!(
480 "Failed to parse parameters for History operation at '{}': {}",
481 pointer.path(),
482 e
483 ))
484 })?;
485
486 if let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) {
487 let target = state.resolve_fixture(target_id)?;
488
489 return Ok(FHIRRequest::History(HistoryRequest::Instance(
490 FHIRHistoryInstanceRequest {
491 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
492 id: target
493 .get_field("id")
494 .ok_or_else(|| {
495 TestScriptError::ExecutionError(format!(
496 "Target fixture '{}' does not have an 'id' field.",
497 target_id
498 ))
499 })?
500 .as_any()
501 .downcast_ref::<String>()
502 .cloned()
503 .unwrap_or_default(),
504 parameters: processed_query,
505 },
506 )));
507 } else if operation.resource.is_some() {
508 let resource_type = derive_resource_type(operation, None, pointer.path())?;
509 return Ok(FHIRRequest::History(HistoryRequest::Type(
510 FHIRHistoryTypeRequest {
511 resource_type,
512 parameters: processed_query,
513 },
514 )));
515 } else {
516 return Ok(FHIRRequest::History(HistoryRequest::System(
517 FHIRHistorySystemRequest {
518 parameters: processed_query,
519 },
520 )));
521 }
522 } else if operation_type == (&TestscriptOperationCodes::Transaction(None)).into() {
523 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
524 return Err(TestScriptError::ExecutionError(format!(
525 "Transaction operation requires sourceId at '{}'.",
526 pointer.path()
527 )));
528 };
529
530 let source = state.resolve_fixture(source_id)?;
531 let resource = (source as &dyn Any)
532 .downcast_ref::<Resource>()
533 .cloned()
534 .ok_or_else(|| {
535 TestScriptError::ExecutionError(format!(
536 "Target fixture '{}' is not a Resource.",
537 source_id
538 ))
539 })?;
540
541 match resource {
542 Resource::Bundle(bundle) => {
543 if !matches!(bundle.type_.as_ref(), BundleType::Transaction(_)) {
544 return Err(TestScriptError::ExecutionError(format!(
545 "Fixture must be a transaction bundle for transaction operations for sourceId '{}'.",
546 source_id
547 )));
548 }
549
550 Ok(FHIRRequest::Transaction(FHIRTransactionRequest {
551 resource: bundle,
552 }))
553 }
554
555 _ => Err(TestScriptError::ExecutionError(format!(
556 "Fixture '{}' is not a transaction Bundle resource.",
557 source_id
558 ))),
559 }
560 } else if operation_type == (&TestscriptOperationCodes::Create(None)).into() {
561 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
562 return Err(TestScriptError::ExecutionError(format!(
563 "Create operation requires sourceId at '{}'.",
564 pointer.path()
565 )));
566 };
567
568 let source = state.resolve_fixture(source_id)?;
569 let resource = (source as &dyn Any)
570 .downcast_ref::<Resource>()
571 .cloned()
572 .ok_or_else(|| {
573 TestScriptError::ExecutionError(format!(
574 "Target fixture '{}' is not a Resource.",
575 source_id
576 ))
577 })?;
578
579 Ok(FHIRRequest::Create(FHIRCreateRequest {
580 resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
581 resource: resource,
582 }))
583 } else if operation_type == (&TestscriptOperationCodes::Update(None)).into() {
584 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
585 return Err(TestScriptError::ExecutionError(format!(
586 "Update operation requires sourceId at '{}'.",
587 pointer.path()
588 )));
589 };
590 let source = state.resolve_fixture(source_id)?;
591 let resource = (source as &dyn Any)
592 .downcast_ref::<Resource>()
593 .cloned()
594 .ok_or_else(|| {
595 TestScriptError::ExecutionError(format!(
596 "Source fixture '{}' is not a Resource.",
597 source_id
598 ))
599 })?;
600
601 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
602 return Err(TestScriptError::ExecutionError(format!(
603 "Update operation requires targetId at '{}'.",
604 pointer.path()
605 )));
606 };
607
608 let target = state.resolve_fixture(target_id)?;
609 let target_resource = (target as &dyn Any)
610 .downcast_ref::<Resource>()
611 .cloned()
612 .ok_or_else(|| {
613 TestScriptError::ExecutionError(format!(
614 "Source fixture '{}' is not a Resource.",
615 source_id
616 ))
617 })?;
618
619 Ok(FHIRRequest::Update(UpdateRequest::Instance(
620 FHIRUpdateInstanceRequest {
621 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
622 id: target_resource
623 .get_field("id")
624 .ok_or_else(|| {
625 TestScriptError::ExecutionError(format!(
626 "Source fixture '{}' does not have an 'id' field.",
627 source_id
628 ))
629 })?
630 .as_any()
631 .downcast_ref::<String>()
632 .cloned()
633 .unwrap_or_default(),
634 resource: resource,
635 },
636 )))
637 } else if operation_type == (&TestscriptOperationCodes::Delete(None)).into() {
638 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
639 return Err(TestScriptError::ExecutionError(format!(
640 "Delete operation requires targetId at '{}'.",
641 pointer.path()
642 )));
643 };
644
645 let target = state.resolve_fixture(target_id)?;
646
647 Ok(FHIRRequest::Delete(DeleteRequest::Instance(
648 FHIRDeleteInstanceRequest {
649 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
650 id: target
651 .get_field("id")
652 .ok_or_else(|| {
653 TestScriptError::ExecutionError(format!(
654 "Target fixture '{}' does not have an 'id' field.",
655 target_id
656 ))
657 })?
658 .as_any()
659 .downcast_ref::<String>()
660 .cloned()
661 .unwrap_or_default(),
662 },
663 )))
664 } else if operation_type == (&TestscriptOperationCodes::DeleteCondMultiple(None)).into() {
665 let delete_parameters = ParsedParameters::try_from(
666 operation
667 .params
668 .as_ref()
669 .and_then(|p| p.value.as_ref())
670 .cloned()
671 .unwrap_or("".to_string())
672 .as_str(),
673 )
674 .map_err(|e| {
675 TestScriptError::ExecutionError(format!(
676 "Failed to parse parameters for DeleteCondMultiple operation at '{}': {}",
677 pointer.path(),
678 e
679 ))
680 })?;
681 if operation.resource.is_some() {
682 Ok(FHIRRequest::Delete(DeleteRequest::Type(
683 FHIRDeleteTypeRequest {
684 resource_type: derive_resource_type(operation, None, pointer.path())?,
685 parameters: delete_parameters,
686 },
687 )))
688 } else {
689 Ok(FHIRRequest::Delete(DeleteRequest::System(
690 FHIRDeleteSystemRequest {
691 parameters: delete_parameters,
692 },
693 )))
694 }
695 } else if operation_type == Some("invoke".to_string()) {
696 let Some(op_code) = operation.url.as_ref().and_then(|u| u.value.as_ref()) else {
697 return Err(TestScriptError::ExecutionError(format!(
698 "Invoke operation requires url at '{}' which is used for the operation code.",
699 pointer.path()
700 )));
701 };
702
703 let op_code = Operation::new(op_code).map_err(|_e| {
704 TestScriptError::ExecutionError(format!(
705 "Invalid operation code for invoke operation '{}'",
706 op_code
707 ))
708 })?;
709
710 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
711 return Err(TestScriptError::ExecutionError(format!(
712 "Invoke operation requires sourceId at '{}'.",
713 pointer.path()
714 )));
715 };
716 let source = state.resolve_fixture(source_id)?;
717 let Resource::Parameters(parameters) = (source as &dyn Any)
718 .downcast_ref::<Resource>()
719 .cloned()
720 .ok_or_else(|| {
721 TestScriptError::ExecutionError(format!(
722 "Source fixture '{}' is not a Resource.",
723 source_id
724 ))
725 })?
726 else {
727 return Err(TestScriptError::ExecutionError(format!(
728 "Source fixture '{}' is not a Parameters resource.",
729 source_id
730 )));
731 };
732
733 if let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) {
734 let target = state.resolve_fixture(target_id)?;
735 let target_resource = (target as &dyn Any)
736 .downcast_ref::<Resource>()
737 .cloned()
738 .ok_or_else(|| {
739 TestScriptError::ExecutionError(format!(
740 "Source fixture '{}' is not a Resource.",
741 source_id
742 ))
743 })?;
744 let target_id = target_resource
745 .get_field("id")
746 .ok_or_else(|| {
747 TestScriptError::ExecutionError(format!(
748 "Source fixture '{}' does not have an 'id' field.",
749 source_id
750 ))
751 })?
752 .as_any()
753 .downcast_ref::<String>()
754 .cloned()
755 .unwrap_or_default();
756 let resource_type = derive_resource_type(operation, Some(target), pointer.path())?;
757
758 Ok(FHIRRequest::Invocation(InvocationRequest::Instance(
759 FHIRInvokeInstanceRequest {
760 operation: op_code,
761 resource_type,
762 id: target_id,
763 parameters,
764 },
765 )))
766 } else if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
767 Ok(FHIRRequest::Invocation(InvocationRequest::Type(
768 FHIRInvokeTypeRequest {
769 operation: op_code,
770 resource_type,
771 parameters,
772 },
773 )))
774 } else {
775 Ok(FHIRRequest::Invocation(InvocationRequest::System(
776 FHIRInvokeSystemRequest {
777 operation: op_code,
778 parameters,
779 },
780 )))
781 }
782 } else {
783 Err(TestScriptError::ExecutionError(format!(
784 "Unsupported TestScript operation type: {:?} at '{}'.",
785 operation_type,
786 pointer.path()
787 )))
788 }
789}
790
791async fn run_operation<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
792 client: &Client,
793 ctx: CTX,
794 state: Arc<Mutex<TestState>>,
795 pointer: TypedPointer<TestScript, TestScriptSetupActionOperation>,
796 options: Arc<TestRunnerOptions>,
797) -> Result<TestResult<TestReportSetupActionOperation>, TestScriptError> {
798 let operation = pointer.value().ok_or_else(|| {
799 TestScriptError::ExecutionError(format!(
800 "Failed to retrieve TestScript operation at '{}'.",
801 pointer.path()
802 ))
803 })?;
804
805 let mut state_guard = state.lock().await;
806 let fhir_request = testscript_operation_to_fhir_request(&state_guard, &pointer).await?;
807 let fhir_response = client.request(ctx, fhir_request.clone()).await;
808 if let Some(wait_duration) = options.wait_between_operations {
809 tokio::time::sleep(wait_duration).await;
810 }
811
812 match fhir_response {
813 Ok(fhir_response) => {
814 associate_request_response_variables(
815 &mut state_guard,
816 operation,
817 fhir_request,
818 Response::FHIRResponse(fhir_response),
819 );
820
821 drop(state_guard);
822
823 Ok(TestResult {
824 state: state.clone(),
825 value: TestReportSetupActionOperation {
826 result: Box::new(ReportActionResultCodes::Pass(None)),
827 ..Default::default()
828 },
829 })
830 }
831 Err(op_error) => {
832 let op_error = Arc::new(op_error);
833 tracing::warn!("Operation at '{}' failed: {}", pointer.path(), op_error);
834 associate_request_response_variables(
835 &mut state_guard,
836 operation,
837 fhir_request,
838 Response::OperationError(op_error.clone()),
839 );
840
841 Ok(TestResult {
842 state: state.clone(),
843 value: TestReportSetupActionOperation {
844 result: Box::new(ReportActionResultCodes::Warning(None)),
845 message: Some(Box::new(FHIRMarkdown {
846 value: Some(format!("Operation failed: {}", op_error)),
847 ..Default::default()
848 })),
849 ..Default::default()
850 },
851 })
852 }
853 }
854}
855
856static DEFAULT_DIRECTION: LazyLock<Box<AssertDirectionCodes>> =
857 LazyLock::new(|| Box::new(AssertDirectionCodes::Response(None)));
858
859async fn get_source<'a>(
860 state: &'a TestState,
861 assertion: &TestScriptSetupActionAssert,
862) -> Result<Option<&'a dyn MetaValue>, TestScriptError> {
863 if let Some(source_id) = assertion.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
864 let source = state.resolve_fixture(source_id)?;
865 Ok(Some(source))
866 } else {
867 match assertion
868 .direction
869 .as_ref()
870 .unwrap_or(&DEFAULT_DIRECTION)
871 .as_ref()
872 {
873 AssertDirectionCodes::Request(_) => {
874 if let Some(request) = state.latest_request.as_ref() {
875 request_to_meta_value(request)
876 .ok_or_else(|| TestScriptError::InvalidFixture)
877 .map(Some)
878 } else {
879 Ok(None)
880 }
881 }
882 AssertDirectionCodes::Response(_) => {
883 if let Some(response) = state.latest_response.as_ref() {
884 response_to_meta_value(response)
885 .ok_or_else(|| TestScriptError::InvalidFixture)
886 .map(Some)
887 } else {
888 Ok(None)
889 }
890 }
891 AssertDirectionCodes::Null(_) => Err(TestScriptError::ExecutionError(
892 "Assert direction cannot be 'null' when sourceId is not provided.".to_string(),
893 )),
894 }
895 }
896}
897
898fn evaluate_operator(
899 operator: &Box<AssertOperatorCodes>,
900 a: &Vec<conversion::ConvertedValue>,
901 b: &Vec<conversion::ConvertedValue>,
902) -> bool {
903 match operator.as_ref() {
904 AssertOperatorCodes::Equals(_) | AssertOperatorCodes::Null(_) => a == b,
905 AssertOperatorCodes::NotEquals(_) => !(a == b),
906
907 AssertOperatorCodes::Contains(_) => {
908 if a.len() != 1 || b.len() != 1 {
909 return false;
910 }
911
912 match (&a[0], &b[0]) {
913 (ConvertedValue::String(a_str), ConvertedValue::String(b_str)) => {
914 a_str.contains(b_str)
915 }
916 _ => false,
917 }
918 }
919 AssertOperatorCodes::Empty(_) => todo!("Empty operator not implemented"),
920 AssertOperatorCodes::Eval(_) => todo!("Eval operator not implemented"),
921 AssertOperatorCodes::GreaterThan(_) => todo!("GreaterThan operator not implemented"),
922 AssertOperatorCodes::In(_) => todo!("In operator not implemented"),
923 AssertOperatorCodes::LessThan(_) => todo!("LessThan operator not implemented"),
924 AssertOperatorCodes::NotContains(_) => todo!("NotContains operator not implemented"),
925 AssertOperatorCodes::NotEmpty(_) => todo!("NotEmpty operator not implemented"),
926 AssertOperatorCodes::NotIn(_) => todo!("NotIn operator not implemented"),
927 }
928 }
930
931static DEFAULT_EQUAL_OPERATOR: LazyLock<Box<AssertOperatorCodes>> =
932 LazyLock::new(|| Box::new(AssertOperatorCodes::Equals(None)));
933
934async fn derive_comparison_to(
935 state: &TestState,
936 assertion: &TestScriptSetupActionAssert,
937) -> Result<Vec<ConvertedValue>, TestScriptError> {
938 if let Some(comparision_fixture_id) = assertion
939 .compareToSourceId
940 .as_ref()
941 .and_then(|c| c.value.as_ref())
942 {
943 let comparison_fixture = state.resolve_fixture(comparision_fixture_id)?;
944
945 let Some(comparison_expression) = assertion
946 .compareToSourceExpression
947 .as_ref()
948 .and_then(|exp| exp.value.as_ref())
949 else {
950 return Err(TestScriptError::ExecutionError(
951 "compareToSourceExpression is required when compareToSourceId is provided."
952 .to_string(),
953 ));
954 };
955
956 let result = state
957 .fp_engine
958 .evaluate(comparison_expression, vec![comparison_fixture])
959 .await
960 .map_err(|e| {
961 TestScriptError::ExecutionError(format!(
962 "FHIRPath evaluation error for comparison fixture '{}': {}",
963 comparision_fixture_id, e
964 ))
965 })?;
966
967 result
968 .iter()
969 .map(|d| {
970 conversion::convert_meta_value(d).ok_or_else(|| {
971 TestScriptError::ExecutionError(format!(
972 "Failed to convert comparison fixture value '{}'.",
973 d.typename()
974 ))
975 })
976 })
977 .collect::<Result<Vec<_>, TestScriptError>>()
978 } else if let Some(value) = assertion.value.as_ref().and_then(|v| v.value.as_ref())
979 && let Some(converted_value) = conversion::convert_string_value(value.as_ref())
980 {
981 Ok(vec![converted_value])
982 } else {
983 Err(TestScriptError::ExecutionError(
984 "Failed to derive comparison value for assertion.".to_string(),
985 ))
986 }
987}
988
989fn get_id<T: MetaValue>(pointer: &TypedPointer<TestScript, T>) -> String {
990 pointer
991 .root()
992 .value()
993 .and_then(|t| t.id.clone())
994 .unwrap_or_default()
995}
996
997async fn run_assertion(
1000 state: Arc<Mutex<TestState>>,
1001 pointer: TypedPointer<TestScript, TestScriptSetupActionAssert>,
1002) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
1003 let assertion = pointer.value().ok_or_else(|| {
1004 TestScriptError::ExecutionError(format!(
1005 "Failed to retrieve TestScript assertion at '{}'.",
1006 pointer.path()
1007 ))
1008 })?;
1009
1010 let mut state_guard = state.lock().await;
1011
1012 let Some(source) = get_source(&*state_guard, assertion).await? else {
1013 return Err(TestScriptError::ExecutionError(format!(
1014 "Failed to resolve source for assertion at '{}'.",
1015 pointer.path()
1016 )));
1017 };
1018
1019 let operator = assertion
1020 .operator
1021 .as_ref()
1022 .unwrap_or(&*DEFAULT_EQUAL_OPERATOR);
1023
1024 if assertion.resource.is_some() {
1025 let resource_string = assertion
1026 .resource
1027 .as_ref()
1028 .and_then(|r| {
1029 let string_type: Option<String> = r.as_ref().into();
1030 string_type
1031 })
1032 .unwrap_or("".to_string());
1033
1034 let operation_evaluation_result = evaluate_operator(
1035 operator,
1036 &vec![conversion::ConvertedValue::String(resource_string.clone())],
1037 &vec![conversion::ConvertedValue::String(
1038 source.typename().to_string(),
1039 )],
1040 );
1041 if !operation_evaluation_result {
1042 tracing::error!(
1043 "{} Assertion at '{}' failed: resource type '{}' does not match '{}'.",
1044 get_id(&pointer),
1045 pointer.path(),
1046 resource_string,
1047 source.typename()
1048 );
1049
1050 state_guard.result = ReportResultCodes::Fail(None);
1051 return Ok(TestResult {
1052 state: state.clone(),
1053 value: TestReportSetupActionAssert {
1054 result: Box::new(ReportActionResultCodes::Fail(None)),
1055 ..Default::default()
1056 },
1057 });
1058 }
1059 }
1060 if let Some(expression) = assertion.expression.as_ref().and_then(|e| e.value.as_ref()) {
1061 let comparison_to = derive_comparison_to(&state_guard, assertion).await?;
1062
1063 let Ok(result) = state_guard
1064 .fp_engine
1065 .evaluate(expression, vec![source])
1066 .await
1067 else {
1068 tracing::error!(
1069 "{} Assertion at '{}' failed: FHIRPath expression '{}' failed to evaluate.",
1070 get_id(&pointer),
1071 expression,
1072 pointer.path()
1073 );
1074
1075 state_guard.result = ReportResultCodes::Fail(None);
1076 return Err(TestScriptError::ExecutionError(format!(
1077 "FHIRPath failed to evaluate at '{}' error.",
1078 pointer.path()
1079 )));
1080 };
1081
1082 let converted_values = result
1083 .iter()
1084 .filter_map(|v| conversion::convert_meta_value(v))
1085 .collect::<Vec<_>>();
1086
1087 let operation_evaluation_result =
1088 evaluate_operator(operator, &converted_values, &comparison_to);
1089
1090 if !operation_evaluation_result {
1091 tracing::error!(
1092 "{} Assertion at '{}' failed: '{:?}' {:?} '{:?}'.",
1093 get_id(&pointer),
1094 pointer.path(),
1095 converted_values,
1096 operator,
1097 comparison_to
1098 );
1099
1100 state_guard.result = ReportResultCodes::Fail(None);
1101 return Ok(TestResult {
1102 state: state.clone(),
1103 value: TestReportSetupActionAssert {
1104 result: Box::new(ReportActionResultCodes::Fail(None)),
1105 ..Default::default()
1106 },
1107 });
1108 }
1109 }
1110
1111 return Ok(TestResult {
1112 state: state.clone(),
1113 value: TestReportSetupActionAssert {
1114 result: Box::new(ReportActionResultCodes::Pass(None)),
1115 ..Default::default()
1116 },
1117 });
1118}
1119
1120async fn run_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1121 client: &Client,
1122 ctx: CTX,
1123 state: Arc<Mutex<TestState>>,
1124 pointer: TypedPointer<TestScript, TestScriptTestAction>,
1125 options: Arc<TestRunnerOptions>,
1126) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1127 tracing::info!("Running TestScript action at path: {}", pointer.path());
1128 let action = pointer.value().ok_or_else(|| {
1129 TestScriptError::ExecutionError(format!(
1130 "Failed to retrieve TestScript action at '{}'.",
1131 pointer.path()
1132 ))
1133 })?;
1134
1135 if action.operation.is_some() {
1138 let Some(operation_pointer) =
1139 pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1140 else {
1141 return Err(TestScriptError::ExecutionError(format!(
1142 "Failed to retrieve TestScript operation at '{}'.",
1143 pointer.path()
1144 )));
1145 };
1146
1147 let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1148
1149 Ok(TestResult {
1150 state: result.state,
1151 value: TestReportSetupAction {
1152 operation: Some(result.value),
1153 ..Default::default()
1154 },
1155 })
1156 } else if action.assert.is_some() {
1157 let Some(assertion_pointer) =
1158 pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1159 else {
1160 return Err(TestScriptError::ExecutionError(format!(
1161 "Failed to retrieve TestScript assertion at '{}'.",
1162 pointer.path()
1163 )));
1164 };
1165
1166 let assertion = run_assertion(state, assertion_pointer).await?;
1167
1168 Ok(TestResult {
1169 state: assertion.state,
1170 value: TestReportSetupAction {
1171 assert: Some(assertion.value),
1172 ..Default::default()
1173 },
1174 })
1175 } else {
1176 Err(TestScriptError::ExecutionError(format!(
1177 "TestScript action must have either an operation or an assert at '{}'.",
1178 pointer.path()
1179 )))
1180 }
1181}
1182
1183async fn run_setup_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1184 client: &Client,
1185 ctx: CTX,
1186 state: Arc<Mutex<TestState>>,
1187 pointer: TypedPointer<TestScript, TestScriptSetupAction>,
1188 options: Arc<TestRunnerOptions>,
1189) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1190 let action = pointer.value().ok_or_else(|| {
1191 TestScriptError::ExecutionError(format!(
1192 "Failed to retrieve TestScript action at '{}'.",
1193 pointer.path()
1194 ))
1195 })?;
1196
1197 tracing::info!("Running TestScript action at path: {}", pointer.path());
1198
1199 if action.operation.is_some() {
1202 let Some(operation_pointer) =
1203 pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1204 else {
1205 return Err(TestScriptError::ExecutionError(format!(
1206 "Failed to retrieve TestScript operation at '{}'.",
1207 pointer.path()
1208 )));
1209 };
1210
1211 let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1212
1213 Ok(TestResult {
1214 state: result.state,
1215 value: TestReportSetupAction {
1216 operation: Some(result.value),
1217 ..Default::default()
1218 },
1219 })
1220 } else if action.assert.is_some() {
1221 let Some(assertion_pointer) =
1222 pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1223 else {
1224 return Err(TestScriptError::ExecutionError(format!(
1225 "Failed to retrieve TestScript assertion at '{}'.",
1226 pointer.path()
1227 )));
1228 };
1229
1230 let assertion = run_assertion(state, assertion_pointer).await?;
1231
1232 Ok(TestResult {
1233 state: assertion.state,
1234 value: TestReportSetupAction {
1235 assert: Some(assertion.value),
1236 ..Default::default()
1237 },
1238 })
1239 } else {
1240 Err(TestScriptError::ExecutionError(format!(
1241 "TestScript action must have either an operation or an assert at '{}'.",
1242 pointer.path()
1243 )))
1244 }
1245}
1246
1247async fn setup_fixtures<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1248 client: &Client,
1249 ctx: CTX,
1250 state: Arc<Mutex<TestState>>,
1251 pointer: TypedPointer<TestScript, TestScript>,
1252 _options: Arc<TestRunnerOptions>,
1253) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
1254 let mut state_lock = state.lock().await;
1255
1256 let Some(fixtures_pointer) =
1257 pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
1258 else {
1259 return Ok(state.clone());
1260 };
1261
1262 let Some(fixtures) = fixtures_pointer.value() else {
1263 return Ok(state.clone());
1264 };
1265
1266 for fixture in fixtures.iter() {
1267 if let Some(reference_string) = fixture
1268 .resource
1269 .as_ref()
1270 .and_then(|r| r.reference.as_ref())
1271 .and_then(|refe| refe.value.as_ref())
1272 {
1273 let resolved_resource = if reference_string.starts_with('#')
1274 && let Some(contained) =
1275 pointer.descend::<Vec<Box<Resource>>>(&Key::Field("contained".to_string()))
1276 && let Some(contained) = contained.value()
1277 {
1278 let local_id = &reference_string[1..];
1279 let Some(resource) = contained.iter().find(|res| {
1280 if let Some(id) = res.get_field("id")
1281 && let Some(id) = id.as_any().downcast_ref::<String>()
1282 {
1283 id.as_str() == local_id
1284 } else {
1285 false
1286 }
1287 }) else {
1288 return Err(OperationOutcomeError::error(
1289 IssueType::NotFound(None),
1290 format!("Contained resource with id '{}' not found.", local_id),
1291 ));
1292 };
1293
1294 resource.as_ref().clone()
1295 } else {
1296 let parts = reference_string.split("/").collect::<Vec<&str>>();
1297 if parts.len() != 2 {
1298 return Err(OperationOutcomeError::error(
1299 IssueType::Invalid(None),
1300 format!("Invalid fixture reference: {}", reference_string),
1301 ));
1302 }
1303
1304 let resource_type = parts[0];
1305 let id = parts[1];
1306
1307 let Some(remote_resource) = client
1308 .read(
1309 ctx.clone(),
1310 ResourceType::try_from(resource_type).map_err(|_| {
1311 OperationOutcomeError::error(
1312 IssueType::Invalid(None),
1313 format!(
1314 "Invalid resource type in fixture reference: '{}'",
1315 resource_type
1316 ),
1317 )
1318 })?,
1319 id.to_string(),
1320 )
1321 .await?
1322 else {
1323 return Err(OperationOutcomeError::error(
1324 IssueType::NotFound(None),
1325 format!("Resource '{}' with id '{}' not found.", resource_type, id),
1326 ));
1327 };
1328
1329 remote_resource
1330 };
1331
1332 state_lock.fixtures.insert(
1333 fixture.id.clone().unwrap_or_default(),
1334 Fixtures::Resource(resolved_resource),
1335 );
1336 }
1337 }
1338
1339 drop(state_lock);
1340
1341 Ok(state)
1342}
1343
1344async fn run_setup<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1345 client: &Client,
1346 ctx: CTX,
1347 state: Arc<Mutex<TestState>>,
1348 pointer: TypedPointer<TestScript, TestScriptSetup>,
1349 options: Arc<TestRunnerOptions>,
1350) -> Result<TestResult<TestReportSetup>, TestScriptError> {
1351 let mut cur_state = state;
1352
1353 let mut setup_results = TestReportSetup {
1354 action: vec![],
1355 ..Default::default()
1356 };
1357
1358 let Some(setup) = pointer.value() else {
1359 return Ok(TestResult {
1360 state: cur_state,
1361 value: setup_results,
1362 });
1363 };
1364
1365 for action in setup.action.iter().enumerate() {
1366 let action_pointer = pointer
1367 .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
1368 .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
1369
1370 let action_pointer = action_pointer.ok_or_else(|| {
1371 TestScriptError::ExecutionError(format!(
1372 "Failed to retrieve TestScript action at index {}.",
1373 action.0
1374 ))
1375 })?;
1376
1377 let result = run_setup_action(
1378 client,
1379 ctx.clone(),
1380 cur_state,
1381 action_pointer,
1382 options.clone(),
1383 )
1384 .await?;
1385 cur_state = result.state;
1386
1387 setup_results.action.push(result.value);
1388 }
1389
1390 Ok(TestResult {
1391 state: cur_state,
1392 value: setup_results,
1393 })
1394}
1395
1396async fn run_teardown<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1397 client: &Client,
1398 ctx: CTX,
1399 state: Arc<Mutex<TestState>>,
1400 pointer: TypedPointer<TestScript, TestScriptTeardown>,
1401 options: Arc<TestRunnerOptions>,
1402) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
1403 let mut cur_state = state;
1404
1405 let mut teardown_results = TestReportTeardown {
1406 action: vec![],
1407 ..Default::default()
1408 };
1409
1410 let Some(actions) = pointer.value() else {
1411 return Ok(TestResult {
1412 state: cur_state,
1413 value: teardown_results,
1414 });
1415 };
1416
1417 for action in actions.action.iter().enumerate() {
1418 let action_pointer = pointer
1419 .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
1420 .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
1421
1422 let action_pointer = action_pointer.ok_or_else(|| {
1423 TestScriptError::ExecutionError(format!(
1424 "Failed to retrieve TestScript teardown action at index {}.",
1425 action.0
1426 ))
1427 })?;
1428
1429 let operation_pointer = action_pointer
1430 .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1431 .ok_or_else(|| {
1432 TestScriptError::ExecutionError(format!(
1433 "Failed to retrieve TestScript teardown operation at index {}.",
1434 action.0
1435 ))
1436 })?;
1437
1438 let result = run_operation(
1439 client,
1440 ctx.clone(),
1441 cur_state,
1442 operation_pointer,
1443 options.clone(),
1444 )
1445 .await?;
1446 cur_state = result.state;
1447
1448 teardown_results.action.push(TestReportTeardownAction {
1449 operation: result.value,
1450 ..Default::default()
1451 });
1452 }
1453
1454 Ok(TestResult {
1455 state: cur_state,
1456 value: teardown_results,
1457 })
1458}
1459
1460async fn run_test<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1461 client: &Client,
1462 ctx: CTX,
1463 state: Arc<Mutex<TestState>>,
1464 pointer: TypedPointer<TestScript, TestScriptTest>,
1465 options: Arc<TestRunnerOptions>,
1466) -> Result<TestResult<TestReportTest>, TestScriptError> {
1467 let mut cur_state = state;
1468 let mut test_report_test = TestReportTest {
1469 action: vec![],
1470 ..Default::default()
1471 };
1472
1473 let test = pointer.value().ok_or_else(|| {
1474 TestScriptError::ExecutionError(format!(
1475 "Failed to retrieve TestScript test at '{}'.",
1476 pointer.path()
1477 ))
1478 })?;
1479
1480 for action in test.action.iter().enumerate() {
1481 let Some(action_pointer) = pointer
1482 .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
1483 .and_then(|p| p.descend(&Key::Index(action.0)))
1484 else {
1485 return Err(TestScriptError::ExecutionError(format!(
1486 "Failed to retrieve TestScript test action at index {}.",
1487 action.0
1488 )));
1489 };
1490 let result = run_action(
1491 client,
1492 ctx.clone(),
1493 cur_state,
1494 action_pointer,
1495 options.clone(),
1496 )
1497 .await?;
1498 cur_state = result.state;
1499 test_report_test.action.push(TestReportTestAction {
1500 operation: result.value.operation,
1501 assert: result.value.assert,
1502 ..Default::default()
1503 });
1504 }
1505
1506 Ok(TestResult {
1507 state: cur_state,
1508 value: test_report_test,
1509 })
1510}
1511
1512async fn run_tests<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1513 client: &Client,
1514 ctx: CTX,
1515 state: Arc<Mutex<TestState>>,
1516 pointer: TypedPointer<TestScript, Vec<TestScriptTest>>,
1517 options: Arc<TestRunnerOptions>,
1518) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
1519 let mut test_results = vec![];
1520 let mut cur_state = state;
1521
1522 let Some(tests) = pointer.value() else {
1523 return Ok(TestResult {
1524 state: cur_state,
1525 value: test_results,
1526 });
1527 };
1528
1529 for test in tests.iter().enumerate() {
1530 let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
1531 return Err(TestScriptError::ExecutionError(format!(
1532 "Failed to retrieve TestScript test at index {}.",
1533 test.0
1534 )));
1535 };
1536 let test_result = run_test(
1537 client,
1538 ctx.clone(),
1539 cur_state,
1540 test_pointer,
1541 options.clone(),
1542 )
1543 .await?;
1544 cur_state = test_result.state;
1545 test_results.push(test_result.value);
1546 }
1547
1548 Ok(TestResult {
1549 state: cur_state,
1550 value: test_results,
1551 })
1552}
1553
1554pub struct TestRunnerOptions {
1555 pub wait_between_operations: Option<Duration>,
1556}
1557
1558pub async fn run<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1559 client: &Client,
1560 ctx: CTX,
1561 test_script: Arc<TestScript>,
1562 options: Arc<TestRunnerOptions>,
1563) -> Result<TestReport, TestScriptError> {
1564 tracing::info!("Running TestScript Runner with FHIR Client");
1566
1567 let mut test_report = TestReport {
1568 status: Box::new(ReportStatusCodes::Completed(None)),
1569 testScript: Box::new(Reference {
1570 reference: Some(Box::new(FHIRString {
1571 value: Some(format!(
1572 "Testscript/{}",
1573 test_script.id.clone().unwrap_or_default()
1574 )),
1575 ..Default::default()
1576 })),
1577 ..Default::default()
1578 }),
1579 ..Default::default()
1580 };
1581
1582 let mut state = Arc::new(Mutex::new(TestState::new()));
1583 let pointer = TypedPointer::<TestScript, TestScript>::new(test_script);
1584
1585 state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
1586 .await
1587 .map_err(|e| TestScriptError::OperationError(e))?;
1588
1589 let mut running_state = Ok(());
1590
1591 if let Some(setup_pointer) =
1593 pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
1594 {
1595 tracing::info!("Running TestScript setup...");
1596 let setup_result = run_setup(
1597 client,
1598 ctx.clone(),
1599 state.clone(),
1600 setup_pointer,
1601 options.clone(),
1602 )
1603 .await;
1604 match setup_result {
1605 Ok(res) => {
1606 state = res.state;
1607 test_report.setup = Some(res.value);
1608 }
1609 Err(e) => {
1610 running_state = Err(e);
1611 }
1612 }
1613 }
1614
1615 if running_state.is_ok()
1617 && let Some(test_pointer) =
1618 pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
1619 {
1620 tracing::info!("Running TestScript tests...");
1621 let test_result = run_tests(
1622 client,
1623 ctx.clone(),
1624 state.clone(),
1625 test_pointer,
1626 options.clone(),
1627 )
1628 .await;
1629
1630 match test_result {
1631 Ok(res) => {
1632 state = res.state;
1633 test_report.test = Some(res.value);
1634 }
1635
1636 Err(e) => {
1637 running_state = Err(e);
1638 }
1639 }
1640 }
1641
1642 if let Some(teardown_pointer) =
1643 pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
1644 {
1645 tracing::info!("Running TestScript teardown...");
1646
1647 let result = run_teardown(
1648 client,
1649 ctx.clone(),
1650 state.clone(),
1651 teardown_pointer,
1652 options.clone(),
1653 )
1654 .await?;
1655
1656 test_report.teardown = Some(result.value);
1658 }
1659
1660 running_state?;
1661
1662 let state_guard = state.lock().await;
1663 match &state_guard.result {
1666 ReportResultCodes::Pending(_) => {
1667 test_report.result = Box::new(ReportResultCodes::Pass(None))
1668 }
1669 status => test_report.result = Box::new(status.clone()),
1670 }
1671
1672 Ok(test_report)
1673}