Skip to main content

haste_testscript_runner/
lib.rs

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, Pointer};
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
66// Internal structure to hold current test result and testing fixtures.
67struct 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        // Associate request variable in state
186        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        // Associate response variable in state
197        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
206/// Derive the resource type from operation or from the metavalue if not present on operation.
207fn 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: Pointer<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: &Pointer<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: Pointer<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::Fail(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    // a == b
929}
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
989/// Assertions are what determine the testreports ultimate pass/fail status.
990/// So set that within state here depending on assertion success/failure.
991async fn run_assertion(
992    state: Arc<Mutex<TestState>>,
993    pointer: Pointer<TestScript, TestScriptSetupActionAssert>,
994) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
995    let assertion = pointer.value().ok_or_else(|| {
996        TestScriptError::ExecutionError(format!(
997            "Failed to retrieve TestScript assertion at '{}'.",
998            pointer.path()
999        ))
1000    })?;
1001
1002    let mut state_guard = state.lock().await;
1003
1004    let Some(source) = get_source(&*state_guard, assertion).await? else {
1005        return Err(TestScriptError::ExecutionError(format!(
1006            "Failed to resolve source for assertion at '{}'.",
1007            pointer.path()
1008        )));
1009    };
1010
1011    let operator = assertion
1012        .operator
1013        .as_ref()
1014        .unwrap_or(&*DEFAULT_EQUAL_OPERATOR);
1015
1016    if assertion.resource.is_some() {
1017        let resource_string = assertion
1018            .resource
1019            .as_ref()
1020            .and_then(|r| {
1021                let string_type: Option<String> = r.as_ref().into();
1022                string_type
1023            })
1024            .unwrap_or("".to_string());
1025
1026        let operation_evaluation_result = evaluate_operator(
1027            operator,
1028            &vec![conversion::ConvertedValue::String(resource_string.clone())],
1029            &vec![conversion::ConvertedValue::String(
1030                source.typename().to_string(),
1031            )],
1032        );
1033        if !operation_evaluation_result {
1034            tracing::error!(
1035                "Assertion at '{}' failed: resource type '{}' does not match '{}'.",
1036                pointer.path(),
1037                resource_string,
1038                source.typename()
1039            );
1040
1041            state_guard.result = ReportResultCodes::Fail(None);
1042            return Ok(TestResult {
1043                state: state.clone(),
1044                value: TestReportSetupActionAssert {
1045                    result: Box::new(ReportActionResultCodes::Fail(None)),
1046                    ..Default::default()
1047                },
1048            });
1049        }
1050    }
1051    if let Some(expression) = assertion.expression.as_ref().and_then(|e| e.value.as_ref()) {
1052        let comparison_to = derive_comparison_to(&state_guard, assertion).await?;
1053
1054        let Ok(result) = state_guard
1055            .fp_engine
1056            .evaluate(expression, vec![source])
1057            .await
1058        else {
1059            tracing::error!(
1060                "Assertion at '{}' failed: FHIRPath expression '{}' failed to evaluate.",
1061                expression,
1062                pointer.path()
1063            );
1064
1065            state_guard.result = ReportResultCodes::Fail(None);
1066            return Err(TestScriptError::ExecutionError(format!(
1067                "FHIRPath failed to evaluate at '{}' error.",
1068                pointer.path()
1069            )));
1070        };
1071
1072        let converted_values = result
1073            .iter()
1074            .filter_map(|v| conversion::convert_meta_value(v))
1075            .collect::<Vec<_>>();
1076
1077        let operation_evaluation_result =
1078            evaluate_operator(operator, &converted_values, &comparison_to);
1079
1080        if !operation_evaluation_result {
1081            tracing::error!(
1082                "Assertion at '{}' failed: '{:?}' {:?} '{:?}'.",
1083                pointer.path(),
1084                converted_values,
1085                operator,
1086                comparison_to
1087            );
1088
1089            state_guard.result = ReportResultCodes::Fail(None);
1090            return Ok(TestResult {
1091                state: state.clone(),
1092                value: TestReportSetupActionAssert {
1093                    result: Box::new(ReportActionResultCodes::Fail(None)),
1094                    ..Default::default()
1095                },
1096            });
1097        }
1098    }
1099
1100    return Ok(TestResult {
1101        state: state.clone(),
1102        value: TestReportSetupActionAssert {
1103            result: Box::new(ReportActionResultCodes::Pass(None)),
1104            ..Default::default()
1105        },
1106    });
1107}
1108
1109async fn run_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1110    client: &Client,
1111    ctx: CTX,
1112    state: Arc<Mutex<TestState>>,
1113    pointer: Pointer<TestScript, TestScriptTestAction>,
1114    options: Arc<TestRunnerOptions>,
1115) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1116    tracing::info!("Running TestScript action at path: {}", pointer.path());
1117    let action = pointer.value().ok_or_else(|| {
1118        TestScriptError::ExecutionError(format!(
1119            "Failed to retrieve TestScript action at '{}'.",
1120            pointer.path()
1121        ))
1122    })?;
1123
1124    // Should be either an operation or an assert.
1125    // Both should not exist at the same time.
1126    if action.operation.is_some() {
1127        let Some(operation_pointer) =
1128            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1129        else {
1130            return Err(TestScriptError::ExecutionError(format!(
1131                "Failed to retrieve TestScript operation at '{}'.",
1132                pointer.path()
1133            )));
1134        };
1135
1136        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1137
1138        Ok(TestResult {
1139            state: result.state,
1140            value: TestReportSetupAction {
1141                operation: Some(result.value),
1142                ..Default::default()
1143            },
1144        })
1145    } else if action.assert.is_some() {
1146        let Some(assertion_pointer) =
1147            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1148        else {
1149            return Err(TestScriptError::ExecutionError(format!(
1150                "Failed to retrieve TestScript assertion at '{}'.",
1151                pointer.path()
1152            )));
1153        };
1154
1155        let assertion = run_assertion(state, assertion_pointer).await?;
1156
1157        Ok(TestResult {
1158            state: assertion.state,
1159            value: TestReportSetupAction {
1160                assert: Some(assertion.value),
1161                ..Default::default()
1162            },
1163        })
1164    } else {
1165        Err(TestScriptError::ExecutionError(format!(
1166            "TestScript action must have either an operation or an assert at '{}'.",
1167            pointer.path()
1168        )))
1169    }
1170}
1171
1172async fn run_setup_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1173    client: &Client,
1174    ctx: CTX,
1175    state: Arc<Mutex<TestState>>,
1176    pointer: Pointer<TestScript, TestScriptSetupAction>,
1177    options: Arc<TestRunnerOptions>,
1178) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1179    let action = pointer.value().ok_or_else(|| {
1180        TestScriptError::ExecutionError(format!(
1181            "Failed to retrieve TestScript action at '{}'.",
1182            pointer.path()
1183        ))
1184    })?;
1185
1186    tracing::info!("Running TestScript action at path: {}", pointer.path());
1187
1188    // Should be either an operation or an assert.
1189    // Both should not exist at the same time.
1190    if action.operation.is_some() {
1191        let Some(operation_pointer) =
1192            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1193        else {
1194            return Err(TestScriptError::ExecutionError(format!(
1195                "Failed to retrieve TestScript operation at '{}'.",
1196                pointer.path()
1197            )));
1198        };
1199
1200        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1201
1202        Ok(TestResult {
1203            state: result.state,
1204            value: TestReportSetupAction {
1205                operation: Some(result.value),
1206                ..Default::default()
1207            },
1208        })
1209    } else if action.assert.is_some() {
1210        let Some(assertion_pointer) =
1211            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1212        else {
1213            return Err(TestScriptError::ExecutionError(format!(
1214                "Failed to retrieve TestScript assertion at '{}'.",
1215                pointer.path()
1216            )));
1217        };
1218
1219        let assertion = run_assertion(state, assertion_pointer).await?;
1220
1221        Ok(TestResult {
1222            state: assertion.state,
1223            value: TestReportSetupAction {
1224                assert: Some(assertion.value),
1225                ..Default::default()
1226            },
1227        })
1228    } else {
1229        Err(TestScriptError::ExecutionError(format!(
1230            "TestScript action must have either an operation or an assert at '{}'.",
1231            pointer.path()
1232        )))
1233    }
1234}
1235
1236async fn setup_fixtures<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1237    client: &Client,
1238    ctx: CTX,
1239    state: Arc<Mutex<TestState>>,
1240    pointer: Pointer<TestScript, TestScript>,
1241    _options: Arc<TestRunnerOptions>,
1242) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
1243    let mut state_lock = state.lock().await;
1244
1245    let Some(fixtures_pointer) =
1246        pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
1247    else {
1248        return Ok(state.clone());
1249    };
1250
1251    let Some(fixtures) = fixtures_pointer.value() else {
1252        return Ok(state.clone());
1253    };
1254
1255    for fixture in fixtures.iter() {
1256        if let Some(reference_string) = fixture
1257            .resource
1258            .as_ref()
1259            .and_then(|r| r.reference.as_ref())
1260            .and_then(|refe| refe.value.as_ref())
1261        {
1262            let resolved_resource = if reference_string.starts_with('#')
1263                && let Some(contained) =
1264                    pointer.descend::<Vec<Box<Resource>>>(&Key::Field("contained".to_string()))
1265                && let Some(contained) = contained.value()
1266            {
1267                let local_id = &reference_string[1..];
1268                let Some(resource) = contained.iter().find(|res| {
1269                    if let Some(id) = res.get_field("id")
1270                        && let Some(id) = id.as_any().downcast_ref::<String>()
1271                    {
1272                        id.as_str() == local_id
1273                    } else {
1274                        false
1275                    }
1276                }) else {
1277                    return Err(OperationOutcomeError::error(
1278                        IssueType::NotFound(None),
1279                        format!("Contained resource with id '{}' not found.", local_id),
1280                    ));
1281                };
1282
1283                resource.as_ref().clone()
1284            } else {
1285                let parts = reference_string.split("/").collect::<Vec<&str>>();
1286                if parts.len() != 2 {
1287                    return Err(OperationOutcomeError::error(
1288                        IssueType::Invalid(None),
1289                        format!("Invalid fixture reference: {}", reference_string),
1290                    ));
1291                }
1292
1293                let resource_type = parts[0];
1294                let id = parts[1];
1295
1296                let Some(remote_resource) = client
1297                    .read(
1298                        ctx.clone(),
1299                        ResourceType::try_from(resource_type).map_err(|_| {
1300                            OperationOutcomeError::error(
1301                                IssueType::Invalid(None),
1302                                format!(
1303                                    "Invalid resource type in fixture reference: '{}'",
1304                                    resource_type
1305                                ),
1306                            )
1307                        })?,
1308                        id.to_string(),
1309                    )
1310                    .await?
1311                else {
1312                    return Err(OperationOutcomeError::error(
1313                        IssueType::NotFound(None),
1314                        format!("Resource '{}' with id '{}' not found.", resource_type, id),
1315                    ));
1316                };
1317
1318                remote_resource
1319            };
1320
1321            state_lock.fixtures.insert(
1322                fixture.id.clone().unwrap_or_default(),
1323                Fixtures::Resource(resolved_resource),
1324            );
1325        }
1326    }
1327
1328    drop(state_lock);
1329
1330    Ok(state)
1331}
1332
1333async fn run_setup<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1334    client: &Client,
1335    ctx: CTX,
1336    state: Arc<Mutex<TestState>>,
1337    pointer: Pointer<TestScript, TestScriptSetup>,
1338    options: Arc<TestRunnerOptions>,
1339) -> Result<TestResult<TestReportSetup>, TestScriptError> {
1340    let mut cur_state = state;
1341
1342    let mut setup_results = TestReportSetup {
1343        action: vec![],
1344        ..Default::default()
1345    };
1346
1347    let Some(setup) = pointer.value() else {
1348        return Ok(TestResult {
1349            state: cur_state,
1350            value: setup_results,
1351        });
1352    };
1353
1354    for action in setup.action.iter().enumerate() {
1355        let action_pointer = pointer
1356            .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
1357            .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
1358
1359        let action_pointer = action_pointer.ok_or_else(|| {
1360            TestScriptError::ExecutionError(format!(
1361                "Failed to retrieve TestScript action at index {}.",
1362                action.0
1363            ))
1364        })?;
1365
1366        let result = run_setup_action(
1367            client,
1368            ctx.clone(),
1369            cur_state,
1370            action_pointer,
1371            options.clone(),
1372        )
1373        .await?;
1374        cur_state = result.state;
1375
1376        setup_results.action.push(result.value);
1377    }
1378
1379    Ok(TestResult {
1380        state: cur_state,
1381        value: setup_results,
1382    })
1383}
1384
1385async fn run_teardown<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1386    client: &Client,
1387    ctx: CTX,
1388    state: Arc<Mutex<TestState>>,
1389    pointer: Pointer<TestScript, TestScriptTeardown>,
1390    options: Arc<TestRunnerOptions>,
1391) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
1392    let mut cur_state = state;
1393
1394    let mut teardown_results = TestReportTeardown {
1395        action: vec![],
1396        ..Default::default()
1397    };
1398
1399    let Some(actions) = pointer.value() else {
1400        return Ok(TestResult {
1401            state: cur_state,
1402            value: teardown_results,
1403        });
1404    };
1405
1406    for action in actions.action.iter().enumerate() {
1407        let action_pointer = pointer
1408            .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
1409            .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
1410
1411        let action_pointer = action_pointer.ok_or_else(|| {
1412            TestScriptError::ExecutionError(format!(
1413                "Failed to retrieve TestScript teardown action at index {}.",
1414                action.0
1415            ))
1416        })?;
1417
1418        let operation_pointer = action_pointer
1419            .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1420            .ok_or_else(|| {
1421                TestScriptError::ExecutionError(format!(
1422                    "Failed to retrieve TestScript teardown operation at index {}.",
1423                    action.0
1424                ))
1425            })?;
1426
1427        let result = run_operation(
1428            client,
1429            ctx.clone(),
1430            cur_state,
1431            operation_pointer,
1432            options.clone(),
1433        )
1434        .await?;
1435        cur_state = result.state;
1436
1437        teardown_results.action.push(TestReportTeardownAction {
1438            operation: result.value,
1439            ..Default::default()
1440        });
1441    }
1442
1443    Ok(TestResult {
1444        state: cur_state,
1445        value: teardown_results,
1446    })
1447}
1448
1449async fn run_test<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1450    client: &Client,
1451    ctx: CTX,
1452    state: Arc<Mutex<TestState>>,
1453    pointer: Pointer<TestScript, TestScriptTest>,
1454    options: Arc<TestRunnerOptions>,
1455) -> Result<TestResult<TestReportTest>, TestScriptError> {
1456    let mut cur_state = state;
1457    let mut test_report_test = TestReportTest {
1458        action: vec![],
1459        ..Default::default()
1460    };
1461
1462    let test = pointer.value().ok_or_else(|| {
1463        TestScriptError::ExecutionError(format!(
1464            "Failed to retrieve TestScript test at '{}'.",
1465            pointer.path()
1466        ))
1467    })?;
1468
1469    for action in test.action.iter().enumerate() {
1470        let Some(action_pointer) = pointer
1471            .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
1472            .and_then(|p| p.descend(&Key::Index(action.0)))
1473        else {
1474            return Err(TestScriptError::ExecutionError(format!(
1475                "Failed to retrieve TestScript test action at index {}.",
1476                action.0
1477            )));
1478        };
1479        let result = run_action(
1480            client,
1481            ctx.clone(),
1482            cur_state,
1483            action_pointer,
1484            options.clone(),
1485        )
1486        .await?;
1487        cur_state = result.state;
1488        test_report_test.action.push(TestReportTestAction {
1489            operation: result.value.operation,
1490            assert: result.value.assert,
1491            ..Default::default()
1492        });
1493    }
1494
1495    Ok(TestResult {
1496        state: cur_state,
1497        value: test_report_test,
1498    })
1499}
1500
1501async fn run_tests<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1502    client: &Client,
1503    ctx: CTX,
1504    state: Arc<Mutex<TestState>>,
1505    pointer: Pointer<TestScript, Vec<TestScriptTest>>,
1506    options: Arc<TestRunnerOptions>,
1507) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
1508    let mut test_results = vec![];
1509    let mut cur_state = state;
1510
1511    let Some(tests) = pointer.value() else {
1512        return Ok(TestResult {
1513            state: cur_state,
1514            value: test_results,
1515        });
1516    };
1517
1518    for test in tests.iter().enumerate() {
1519        let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
1520            return Err(TestScriptError::ExecutionError(format!(
1521                "Failed to retrieve TestScript test at index {}.",
1522                test.0
1523            )));
1524        };
1525        let test_result = run_test(
1526            client,
1527            ctx.clone(),
1528            cur_state,
1529            test_pointer,
1530            options.clone(),
1531        )
1532        .await?;
1533        cur_state = test_result.state;
1534        test_results.push(test_result.value);
1535    }
1536
1537    Ok(TestResult {
1538        state: cur_state,
1539        value: test_results,
1540    })
1541}
1542
1543pub struct TestRunnerOptions {
1544    pub wait_between_operations: Option<Duration>,
1545}
1546
1547pub async fn run<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1548    client: &Client,
1549    ctx: CTX,
1550    test_script: Arc<TestScript>,
1551    options: Arc<TestRunnerOptions>,
1552) -> Result<TestReport, TestScriptError> {
1553    // Placeholder implementation
1554    tracing::info!("Running TestScript Runner with FHIR Client");
1555
1556    let mut test_report = TestReport {
1557        status: Box::new(ReportStatusCodes::Completed(None)),
1558        testScript: Box::new(Reference {
1559            reference: Some(Box::new(FHIRString {
1560                value: Some(format!(
1561                    "Testscript/{}",
1562                    test_script.id.clone().unwrap_or_default()
1563                )),
1564                ..Default::default()
1565            })),
1566            ..Default::default()
1567        }),
1568        ..Default::default()
1569    };
1570
1571    let mut state = Arc::new(Mutex::new(TestState::new()));
1572    let pointer = Pointer::<TestScript, TestScript>::new(test_script);
1573
1574    state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
1575        .await
1576        .map_err(|e| TestScriptError::OperationError(e))?;
1577
1578    let mut running_state = Ok(());
1579
1580    // Run setup actions
1581    if let Some(setup_pointer) =
1582        pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
1583    {
1584        tracing::info!("Running TestScript setup...");
1585        let setup_result = run_setup(
1586            client,
1587            ctx.clone(),
1588            state.clone(),
1589            setup_pointer,
1590            options.clone(),
1591        )
1592        .await;
1593        match setup_result {
1594            Ok(res) => {
1595                state = res.state;
1596                test_report.setup = Some(res.value);
1597            }
1598            Err(e) => {
1599                running_state = Err(e);
1600            }
1601        }
1602    }
1603
1604    // Run Test actions
1605    if running_state.is_ok()
1606        && let Some(test_pointer) =
1607            pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
1608    {
1609        tracing::info!("Running TestScript tests...");
1610        let test_result = run_tests(
1611            client,
1612            ctx.clone(),
1613            state.clone(),
1614            test_pointer,
1615            options.clone(),
1616        )
1617        .await;
1618
1619        match test_result {
1620            Ok(res) => {
1621                state = res.state;
1622                test_report.test = Some(res.value);
1623            }
1624
1625            Err(e) => {
1626                running_state = Err(e);
1627            }
1628        }
1629    }
1630
1631    if let Some(teardown_pointer) =
1632        pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
1633    {
1634        tracing::info!("Running TestScript teardown...");
1635
1636        let result = run_teardown(
1637            client,
1638            ctx.clone(),
1639            state.clone(),
1640            teardown_pointer,
1641            options.clone(),
1642        )
1643        .await?;
1644
1645        // state = result.state;
1646        test_report.teardown = Some(result.value);
1647    }
1648
1649    running_state?;
1650
1651    let state_guard = state.lock().await;
1652    // Only set result to fail so if still pending can assume pass.
1653    // Flip to fail in assertion tests if any fail.
1654    match &state_guard.result {
1655        ReportResultCodes::Pending(_) => {
1656            test_report.result = Box::new(ReportResultCodes::Pass(None))
1657        }
1658        status => test_report.result = Box::new(status.clone()),
1659    }
1660
1661    Ok(test_report)
1662}