Skip to main content

haste_testscript_runner/
lib.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
2use haste_fhir_client::{
3    FHIRClient,
4    http::{HeaderMap, HeaderName, HeaderValue, WithRequestHeaders},
5    request::{
6        DeleteRequest, FHIRBatchRequest, FHIRConditionalUpdateRequest, FHIRCreateRequest,
7        FHIRDeleteInstanceRequest, FHIRDeleteSystemRequest, FHIRDeleteTypeRequest,
8        FHIRHistoryInstanceRequest, FHIRHistorySystemRequest, FHIRHistoryTypeRequest,
9        FHIRInvokeInstanceRequest, FHIRInvokeSystemRequest, FHIRInvokeTypeRequest,
10        FHIRPatchRequest, FHIRReadRequest, FHIRRequest, FHIRResponse, FHIRTransactionRequest,
11        FHIRUpdateInstanceRequest, FHIRVersionReadRequest, HistoryRequest, HistoryResponse,
12        InvocationRequest, InvokeResponse, Operation, SearchResponse, UpdateRequest,
13    },
14    url::ParsedParameters,
15};
16use haste_fhir_model::r4::generated::{
17    resources::{
18        Resource, ResourceType, TestReport, TestReportSetup, TestReportSetupAction,
19        TestReportSetupActionAssert, TestReportSetupActionOperation, TestReportTeardown,
20        TestReportTeardownAction, TestReportTest, TestReportTestAction, TestScript,
21        TestScriptFixture, TestScriptSetup, TestScriptSetupAction, TestScriptSetupActionAssert,
22        TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
23        TestScriptTest, TestScriptTestAction, TestScriptVariable,
24    },
25    terminology::{
26        AssertDirectionCodes, AssertOperatorCodes, BoundCode, BundleType, IssueType,
27        ReportActionResultCodes, ReportResultCodes, ReportStatusCodes, TestscriptOperationCodes,
28    },
29    types::{FHIRId, FHIRMarkdown, FHIRString, Reference},
30};
31use haste_fhir_operation_error::OperationOutcomeError;
32use haste_pointer::{Key, TypedPointer};
33use haste_reflect::MetaValue;
34use regex::Regex;
35use std::{
36    any::Any,
37    collections::HashMap,
38    sync::{Arc, LazyLock},
39    time::Duration,
40};
41use tokio::sync::Mutex;
42
43use crate::conversion::ConvertedValue;
44
45mod conversion;
46
47#[derive(Debug)]
48pub enum TestScriptError {
49    ExecutionError(String),
50    ValidationError(String),
51    FixtureNotFound,
52    InvalidFixture,
53    OperationError(OperationOutcomeError),
54}
55
56#[derive(Debug, Clone)]
57enum Response {
58    FHIRResponse(Box<FHIRResponse>),
59    OperationError(Arc<OperationOutcomeError>),
60}
61
62#[derive(Debug)]
63enum Fixtures {
64    Resource(Resource),
65    Request(FHIRRequest),
66    Response(Response),
67}
68
69// Internal structure to hold current test result and testing fixtures.
70struct TestState {
71    fp_engine: haste_fhirpath::FPEngine,
72    fixtures: HashMap<String, Fixtures>,
73    latest_request: Option<FHIRRequest>,
74    latest_response: Option<Response>,
75    result: BoundCode<ReportResultCodes>,
76}
77
78impl TestState {
79    fn new() -> Self {
80        TestState {
81            fp_engine: haste_fhirpath::FPEngine::new(),
82            fixtures: HashMap::new(),
83            latest_request: None,
84            latest_response: None,
85            result: ReportResultCodes::pending(),
86        }
87    }
88    fn resolve_fixture<'a>(
89        &'a self,
90        fixture_id: &str,
91    ) -> Result<&'a dyn MetaValue, TestScriptError> {
92        let fixture = self
93            .fixtures
94            .get(fixture_id)
95            .ok_or(TestScriptError::FixtureNotFound)?;
96
97        match fixture {
98            Fixtures::Resource(res) => Ok(res),
99            Fixtures::Request(req) => {
100                request_to_meta_value(req).ok_or_else(|| TestScriptError::InvalidFixture)
101            }
102            Fixtures::Response(response) => {
103                response_to_meta_value(response).ok_or_else(|| TestScriptError::InvalidFixture)
104            }
105        }
106    }
107}
108
109struct TestResult<T> {
110    pub state: Arc<Mutex<TestState>>,
111    pub value: T,
112}
113
114fn response_to_meta_value(response: &Response) -> Option<&dyn MetaValue> {
115    match response {
116        Response::FHIRResponse(fhir_response) => match &**fhir_response {
117            FHIRResponse::Create(res) => Some(&res.resource),
118            FHIRResponse::Read(res) => Some(&res.resource),
119            FHIRResponse::VersionRead(res) => Some(&res.resource),
120            FHIRResponse::Update(res) => Some(&res.resource),
121            FHIRResponse::Patch(res) => Some(&res.resource),
122            FHIRResponse::Batch(res) => Some(&res.resource),
123            FHIRResponse::Transaction(res) => Some(&res.resource),
124
125            FHIRResponse::Capabilities(res) => Some(&res.capabilities),
126            FHIRResponse::Search(res) => match res {
127                SearchResponse::Type(res) => Some(&res.bundle),
128                SearchResponse::System(res) => Some(&res.bundle),
129            },
130            FHIRResponse::History(res) => match res {
131                HistoryResponse::Instance(res) => Some(&res.bundle),
132                HistoryResponse::Type(res) => Some(&res.bundle),
133                HistoryResponse::System(res) => Some(&res.bundle),
134            },
135            FHIRResponse::Invoke(res) => match res {
136                InvokeResponse::Instance(res) => Some(&res.resource),
137                InvokeResponse::Type(res) => Some(&res.resource),
138                InvokeResponse::System(res) => Some(&res.resource),
139            },
140
141            FHIRResponse::Delete(_) => None,
142        },
143        Response::OperationError(op_error) => {
144            let outcome = op_error.outcome();
145            Some(outcome)
146        }
147    }
148}
149
150fn request_to_meta_value(request: &FHIRRequest) -> Option<&dyn MetaValue> {
151    match request {
152        FHIRRequest::Create(req) => Some(&req.resource),
153
154        FHIRRequest::Update(update_request) => match update_request {
155            UpdateRequest::Conditional(req) => Some(&req.resource),
156            UpdateRequest::Instance(req) => Some(&req.resource),
157        },
158
159        FHIRRequest::Batch(req) => Some(&req.resource),
160        FHIRRequest::Transaction(req) => Some(&req.resource),
161        FHIRRequest::Invocation(req) => match req {
162            haste_fhir_client::request::InvocationRequest::Instance(req) => Some(&req.parameters),
163            haste_fhir_client::request::InvocationRequest::Type(req) => Some(&req.parameters),
164            haste_fhir_client::request::InvocationRequest::System(req) => Some(&req.parameters),
165        },
166        FHIRRequest::Read(_)
167        | FHIRRequest::VersionRead(_)
168        | FHIRRequest::Compartment(_)
169        | FHIRRequest::Patch(_)
170        | FHIRRequest::Delete(_)
171        | FHIRRequest::Capabilities
172        | FHIRRequest::Search(_)
173        | FHIRRequest::History(_) => None,
174    }
175}
176
177fn associate_request_response_variables(
178    state: &mut TestState,
179    operation: &TestScriptSetupActionOperation,
180    request: FHIRRequest,
181    response: Response,
182) {
183    if let Some(request_var) = operation
184        .requestId
185        .as_ref()
186        .and_then(|id| id.value.as_ref())
187    {
188        // Associate request variable in state
189        state
190            .fixtures
191            .insert(request_var.clone(), Fixtures::Request(request.clone()));
192    }
193
194    if let Some(response_var) = operation
195        .responseId
196        .as_ref()
197        .and_then(|id| id.value.as_ref())
198    {
199        // Associate response variable in state
200        state
201            .fixtures
202            .insert(response_var.clone(), Fixtures::Response(response.clone()));
203    }
204
205    state.latest_request = Some(request);
206    state.latest_response = Some(response);
207}
208
209/// Derive the resource type from operation or from the metavalue if not present on operation.
210fn derive_resource_type(
211    operation: &TestScriptSetupActionOperation,
212    target: Option<&dyn MetaValue>,
213    path: &str,
214) -> Result<ResourceType, TestScriptError> {
215    if let Some(operation_resource_type) = operation.resource.as_ref() {
216        let string_type = operation_resource_type.as_str();
217        ResourceType::try_from(string_type.unwrap_or_default()).map_err(|_| {
218            TestScriptError::ExecutionError(format!(
219                "Unsupported resource type '{operation_resource_type:?}' for operation at '{path}'."
220            ))
221        })
222    } else if let Some(target) = target {
223        ResourceType::try_from(target.fhir_type()).map_err(|_| {
224            TestScriptError::ExecutionError(format!(
225                "Unsupported resource type '{}' for operation at '{path}'.",
226                target.fhir_type()
227            ))
228        })
229    } else {
230        Err(TestScriptError::ExecutionError(format!(
231            "Failed to derive resource type for operation at '{path}'.",
232        )))
233    }
234}
235
236static EXPRESSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]*)\}").unwrap());
237
238async fn get_variable(
239    state: &TestState,
240    variables: &[TestScriptVariable],
241    variable_id: &str,
242) -> Result<ConvertedValue, TestScriptError> {
243    let Some(variable) = variables
244        .iter()
245        .find(|v| v.name.value.as_deref() == Some(variable_id))
246    else {
247        return Err(TestScriptError::ExecutionError(format!(
248            "Variable with id '{variable_id}' not found."
249        )));
250    };
251
252    if let Some(expression) = variable
253        .expression
254        .as_ref()
255        .and_then(|exp| exp.value.as_ref())
256    {
257        let values =
258            if let Some(source_id) = variable.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
259                let source = state.resolve_fixture(source_id)?;
260                vec![source]
261            } else {
262                vec![]
263            };
264
265        let eval_result = state
266            .fp_engine
267            .evaluate(expression, values)
268            .await
269            .map_err(|e| {
270                TestScriptError::ExecutionError(format!(
271                    "Failed to evaluate FHIRPath expression for variable '{variable_id}': {e}"
272                ))
273            })?;
274
275        let converted_values = eval_result
276            .iter()
277            .map(conversion::convert_meta_value)
278            .collect::<Vec<_>>();
279
280        if converted_values.len() == 1 {
281            Ok(converted_values.into_iter().next().unwrap())
282        } else {
283            Err(TestScriptError::ExecutionError(format!(
284                "Variable '{variable_id}' evaluation returned multiple values; only single value supported.",
285            )))
286        }
287    } else {
288        Err(TestScriptError::ExecutionError(format!(
289            "Only support variable with expression for variable id '{variable_id}'.",
290        )))
291    }
292}
293
294async fn evaluate_variable(
295    state: &TestState,
296    pointer: TypedPointer<TestScript, TestScript>,
297    value: &str,
298) -> Result<String, TestScriptError> {
299    let mut result = value.to_string();
300    let variable_pointer =
301        pointer.descend::<Vec<TestScriptVariable>>(&Key::Field("variable".to_string()));
302    let default_variables = vec![];
303
304    let variables = if let Some(pointer) = variable_pointer.as_ref() {
305        pointer.value().unwrap_or(&default_variables)
306    } else {
307        &default_variables
308    };
309
310    for reg_match in EXPRESSION_REGEX.captures_iter(value) {
311        let full_match = reg_match.get(0).map_or("", |m| m.as_str());
312        let Some(variable_id) = reg_match.get(1).map(|m| m.as_str()) else {
313            return Err(TestScriptError::ExecutionError(format!(
314                "Invalid variable expression in '{value}'."
315            )));
316        };
317
318        let variable = get_variable(state, variables, variable_id).await?;
319        result = result.replace(full_match, variable.to_string().as_str());
320    }
321
322    Ok(result)
323}
324
325async fn testscript_operation_to_fhir_request(
326    state: &TestState,
327    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
328) -> Result<FHIRRequest, TestScriptError> {
329    let operation = get_operation(pointer)?;
330    let op = get_operation_type(operation);
331
332    match op {
333        Some(op) if Some(op) == TestscriptOperationCodes::read().as_str() => {
334            read_request(state, pointer, operation)
335        }
336
337        Some(op) if Some(op) == TestscriptOperationCodes::vread().as_str() => {
338            version_read_request(state, pointer, operation)
339        }
340
341        Some(op) if Some(op) == TestscriptOperationCodes::search().as_str() => {
342            search_request(state, pointer, operation).await
343        }
344
345        Some(op) if Some(op) == TestscriptOperationCodes::history().as_str() => {
346            history_request(state, pointer, operation).await
347        }
348
349        Some(op) if Some(op) == TestscriptOperationCodes::transaction().as_str() => {
350            transaction_request(state, pointer, operation)
351        }
352
353        Some(op) if Some(op) == TestscriptOperationCodes::batch().as_str() => {
354            batch_request(state, pointer, operation)
355        }
356
357        Some(op) if Some(op) == TestscriptOperationCodes::create().as_str() => {
358            create_request(state, pointer, operation)
359        }
360
361        Some(op) if Some(op) == TestscriptOperationCodes::update().as_str() => {
362            update_request(state, pointer, operation)
363        }
364
365        Some(op) if Some(op) == TestscriptOperationCodes::update_create().as_str() => {
366            conditional_update_request(state, pointer, operation).await
367        }
368
369        Some(op) if Some(op) == TestscriptOperationCodes::patch().as_str() => {
370            patch_request(state, pointer, operation)
371        }
372
373        Some(op) if Some(op) == TestscriptOperationCodes::delete().as_str() => {
374            delete_request(state, pointer, operation)
375        }
376
377        Some(op)
378            if Some(op) == TestscriptOperationCodes::delete_cond_multiple().as_str()
379                || Some(op) == TestscriptOperationCodes::delete_cond_single().as_str() =>
380        {
381            conditional_delete_request(state, pointer, operation)
382        }
383
384        Some(op)
385            if Some(op) == TestscriptOperationCodes::capabilities().as_str()
386                || Some(op) == TestscriptOperationCodes::conforms().as_str() =>
387        {
388            Ok(FHIRRequest::Capabilities)
389        }
390
391        Some("invoke") => invoke_request(state, pointer, operation),
392
393        _ => Err(TestScriptError::ExecutionError(format!(
394            "Unsupported TestScript operation type: {op:?} at '{}'.",
395            pointer.path(),
396        ))),
397    }
398}
399
400fn read_request(
401    state: &TestState,
402    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
403    operation: &TestScriptSetupActionOperation,
404) -> Result<FHIRRequest, TestScriptError> {
405    let target_id = require_target_id(operation, pointer.path(), "Read")?;
406    let target = state.resolve_fixture(target_id)?;
407
408    Ok(FHIRRequest::Read(FHIRReadRequest {
409        resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
410        id: fixture_string_field(target, target_id, "id")?,
411    }))
412}
413
414fn version_read_request(
415    state: &TestState,
416    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
417    operation: &TestScriptSetupActionOperation,
418) -> Result<FHIRRequest, TestScriptError> {
419    let target_id = require_target_id(operation, pointer.path(), "Version Read")?;
420    let target = state.resolve_fixture(target_id)?;
421
422    Ok(FHIRRequest::VersionRead(FHIRVersionReadRequest {
423        resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
424        id: fixture_string_field(target, target_id, "id")?,
425        version_id: fixture_version_id(target, target_id)?.into(),
426    }))
427}
428
429async fn search_request(
430    state: &TestState,
431    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
432    operation: &TestScriptSetupActionOperation,
433) -> Result<FHIRRequest, TestScriptError> {
434    let query = operation
435        .params
436        .as_ref()
437        .and_then(|p| p.value.as_deref())
438        .unwrap_or_default();
439
440    let parameters =
441        parsed_parameters(state, pointer.root(), query, pointer.path(), "Search").await?;
442
443    if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
444        Ok(FHIRRequest::Search(
445            haste_fhir_client::request::SearchRequest::Type(
446                haste_fhir_client::request::FHIRSearchTypeRequest {
447                    resource_type,
448                    parameters,
449                },
450            ),
451        ))
452    } else {
453        Ok(FHIRRequest::Search(
454            haste_fhir_client::request::SearchRequest::System(
455                haste_fhir_client::request::FHIRSearchSystemRequest { parameters },
456            ),
457        ))
458    }
459}
460
461async fn history_request(
462    state: &TestState,
463    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
464    operation: &TestScriptSetupActionOperation,
465) -> Result<FHIRRequest, TestScriptError> {
466    let parameters = parsed_parameters(
467        state,
468        pointer.root(),
469        operation
470            .params
471            .as_ref()
472            .and_then(|p| p.value.as_deref())
473            .unwrap_or_default(),
474        pointer.path(),
475        "History",
476    )
477    .await?;
478
479    if let Some(target_id) = operation
480        .targetId
481        .as_ref()
482        .and_then(|id| id.value.as_deref())
483    {
484        let target = state.resolve_fixture(target_id)?;
485
486        Ok(FHIRRequest::History(HistoryRequest::Instance(
487            FHIRHistoryInstanceRequest {
488                resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
489                id: fixture_string_field(target, target_id, "id")?,
490                parameters,
491            },
492        )))
493    } else if operation.resource.is_some() {
494        Ok(FHIRRequest::History(HistoryRequest::Type(
495            FHIRHistoryTypeRequest {
496                resource_type: derive_resource_type(operation, None, pointer.path())?,
497                parameters,
498            },
499        )))
500    } else {
501        Ok(FHIRRequest::History(HistoryRequest::System(
502            FHIRHistorySystemRequest { parameters },
503        )))
504    }
505}
506
507fn transaction_request(
508    state: &TestState,
509    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
510    operation: &TestScriptSetupActionOperation,
511) -> Result<FHIRRequest, TestScriptError> {
512    let source_id = require_source_id(operation, pointer.path(), "Transaction")?;
513    let source = state.resolve_fixture(source_id)?;
514    let resource = fixture_resource(source, source_id)?;
515
516    match resource {
517        Resource::Bundle(bundle) => {
518            if bundle.type_ != BundleType::transaction() {
519                return Err(TestScriptError::ExecutionError(format!(
520                    "Fixture must be a transaction bundle for transaction operations for sourceId '{source_id}'."
521                )));
522            }
523
524            Ok(FHIRRequest::Transaction(FHIRTransactionRequest {
525                resource: bundle,
526            }))
527        }
528        _ => Err(TestScriptError::ExecutionError(format!(
529            "Fixture '{source_id}' is not a transaction Bundle resource."
530        ))),
531    }
532}
533
534fn batch_request(
535    state: &TestState,
536    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
537    operation: &TestScriptSetupActionOperation,
538) -> Result<FHIRRequest, TestScriptError> {
539    let source_id = require_source_id(operation, pointer.path(), "Batch")?;
540    let source = state.resolve_fixture(source_id)?;
541    let resource = fixture_resource(source, source_id)?;
542
543    match resource {
544        Resource::Bundle(bundle) => {
545            if bundle.type_ != BundleType::batch() {
546                return Err(TestScriptError::ExecutionError(format!(
547                    "Fixture must be a batch bundle for batch operations for sourceId '{source_id}'."
548                )));
549            }
550
551            Ok(FHIRRequest::Batch(FHIRBatchRequest { resource: bundle }))
552        }
553        _ => Err(TestScriptError::ExecutionError(format!(
554            "Fixture '{source_id}' is not a batch Bundle resource."
555        ))),
556    }
557}
558
559fn create_request(
560    state: &TestState,
561    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
562    operation: &TestScriptSetupActionOperation,
563) -> Result<FHIRRequest, TestScriptError> {
564    let source_id = require_source_id(operation, pointer.path(), "Create")?;
565    let source = state.resolve_fixture(source_id)?;
566    let resource = fixture_resource(source, source_id)?;
567
568    Ok(FHIRRequest::Create(FHIRCreateRequest {
569        resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
570        resource,
571    }))
572}
573
574fn update_request(
575    state: &TestState,
576    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
577    operation: &TestScriptSetupActionOperation,
578) -> Result<FHIRRequest, TestScriptError> {
579    let source_id = require_source_id(operation, pointer.path(), "Update")?;
580    let source = state.resolve_fixture(source_id)?;
581    let resource = fixture_resource(source, source_id)?;
582
583    let target_id = require_target_id(operation, pointer.path(), "Update")?;
584    let target = state.resolve_fixture(target_id)?;
585    let target_resource = fixture_resource(target, target_id)?;
586
587    Ok(FHIRRequest::Update(UpdateRequest::Instance(
588        FHIRUpdateInstanceRequest {
589            resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
590            id: fixture_string_field(&target_resource, target_id, "id")?,
591            resource,
592        },
593    )))
594}
595
596async fn conditional_update_request(
597    state: &TestState,
598    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
599    operation: &TestScriptSetupActionOperation,
600) -> Result<FHIRRequest, TestScriptError> {
601    let source_id = require_source_id(operation, pointer.path(), "UpdateCreate")?;
602    let source = state.resolve_fixture(source_id)?;
603    let resource = fixture_resource(source, source_id)?;
604
605    let parameters = parsed_parameters(
606        state,
607        pointer.root(),
608        operation
609            .params
610            .as_ref()
611            .and_then(|p| p.value.as_deref())
612            .unwrap_or_default(),
613        pointer.path(),
614        "UpdateCreate",
615    )
616    .await?;
617
618    Ok(FHIRRequest::Update(UpdateRequest::Conditional(
619        FHIRConditionalUpdateRequest {
620            resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
621            parameters,
622            resource,
623        },
624    )))
625}
626
627fn delete_request(
628    state: &TestState,
629    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
630    operation: &TestScriptSetupActionOperation,
631) -> Result<FHIRRequest, TestScriptError> {
632    let target_id = require_target_id(operation, pointer.path(), "Delete")?;
633    let target = state.resolve_fixture(target_id)?;
634
635    Ok(FHIRRequest::Delete(DeleteRequest::Instance(
636        FHIRDeleteInstanceRequest {
637            resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
638            id: fixture_string_field(target, target_id, "id")?,
639        },
640    )))
641}
642
643fn conditional_delete_request(
644    _state: &TestState,
645    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
646    operation: &TestScriptSetupActionOperation,
647) -> Result<FHIRRequest, TestScriptError> {
648    let parameters = ParsedParameters::try_from(
649        operation
650            .params
651            .as_ref()
652            .and_then(|p| p.value.as_deref())
653            .unwrap_or_default(),
654    )
655    .map_err(|e| {
656        TestScriptError::ExecutionError(format!(
657            "Failed to parse parameters for conditional delete operation at '{}': {}",
658            pointer.path(),
659            e
660        ))
661    })?;
662
663    if operation.resource.is_some() {
664        Ok(FHIRRequest::Delete(DeleteRequest::Type(
665            FHIRDeleteTypeRequest {
666                resource_type: derive_resource_type(operation, None, pointer.path())?,
667                parameters,
668            },
669        )))
670    } else {
671        Ok(FHIRRequest::Delete(DeleteRequest::System(
672            FHIRDeleteSystemRequest { parameters },
673        )))
674    }
675}
676
677/// The patch source fixture must be a `Binary` resource carrying the JSON Patch
678/// document, base64-encoded in `data` with `contentType`
679/// `application/json-patch+json` — the standard way `TestScript` represents patch
680/// bodies, since there's no dedicated FHIR resource type for a JSON Patch document.
681fn patch_request(
682    state: &TestState,
683    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
684    operation: &TestScriptSetupActionOperation,
685) -> Result<FHIRRequest, TestScriptError> {
686    let target_id = require_target_id(operation, pointer.path(), "Patch")?;
687    let target = state.resolve_fixture(target_id)?;
688
689    let source_id = require_source_id(operation, pointer.path(), "Patch")?;
690    let source = state.resolve_fixture(source_id)?;
691    let Resource::Binary(binary) = fixture_resource(source, source_id)? else {
692        return Err(TestScriptError::ExecutionError(format!(
693            "Patch source fixture '{source_id}' must be a Binary resource containing the JSON Patch document."
694        )));
695    };
696
697    let content_type = binary.contentType.value.as_deref().unwrap_or_default();
698    if content_type != "application/json-patch+json" {
699        return Err(TestScriptError::ExecutionError(format!(
700            "Unsupported patch content type '{content_type}' for fixture '{source_id}'; only 'application/json-patch+json' is supported."
701        )));
702    }
703
704    let encoded = binary
705        .data
706        .as_ref()
707        .and_then(|d| d.value.as_deref())
708        .ok_or_else(|| {
709            TestScriptError::ExecutionError(format!(
710                "Patch source fixture '{source_id}' Binary resource has no 'data'."
711            ))
712        })?;
713
714    let decoded = BASE64.decode(encoded).map_err(|e| {
715        TestScriptError::ExecutionError(format!(
716            "Failed to base64-decode patch content for fixture '{source_id}': {e}"
717        ))
718    })?;
719
720    let patch: json_patch::Patch = serde_json::from_slice(&decoded).map_err(|e| {
721        TestScriptError::ExecutionError(format!(
722            "Failed to parse JSON Patch document for fixture '{source_id}': {e}"
723        ))
724    })?;
725
726    Ok(FHIRRequest::Patch(FHIRPatchRequest {
727        resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
728        id: fixture_string_field(target, target_id, "id")?,
729        patch,
730    }))
731}
732
733fn invoke_request(
734    state: &TestState,
735    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
736    operation: &TestScriptSetupActionOperation,
737) -> Result<FHIRRequest, TestScriptError> {
738    let op_code = operation
739        .url
740        .as_ref()
741        .and_then(|u| u.value.as_deref())
742        .ok_or_else(|| {
743            TestScriptError::ExecutionError(format!(
744                "Invoke operation requires url at '{}' which is used for the operation code.",
745                pointer.path()
746            ))
747        })?;
748
749    let fhir_operation = Operation::new(op_code);
750
751    let source_id = require_source_id(operation, pointer.path(), "Invoke")?;
752    let source = state.resolve_fixture(source_id)?;
753
754    let Resource::Parameters(parameters) = fixture_resource(source, source_id)? else {
755        return Err(TestScriptError::ExecutionError(format!(
756            "Source fixture '{source_id}' is not a Parameters resource."
757        )));
758    };
759
760    if let Some(target_id) = operation
761        .targetId
762        .as_ref()
763        .and_then(|id| id.value.as_deref())
764    {
765        let target = state.resolve_fixture(target_id)?;
766
767        Ok(FHIRRequest::Invocation(InvocationRequest::Instance(
768            FHIRInvokeInstanceRequest {
769                operation: fhir_operation,
770                resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
771                id: fixture_string_field(target, target_id, "id")?,
772                parameters,
773            },
774        )))
775    } else if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
776        Ok(FHIRRequest::Invocation(InvocationRequest::Type(
777            FHIRInvokeTypeRequest {
778                operation: fhir_operation,
779                resource_type,
780                parameters,
781            },
782        )))
783    } else {
784        Ok(FHIRRequest::Invocation(InvocationRequest::System(
785            FHIRInvokeSystemRequest {
786                operation: fhir_operation,
787                parameters,
788            },
789        )))
790    }
791}
792
793fn get_operation(
794    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
795) -> Result<&TestScriptSetupActionOperation, TestScriptError> {
796    pointer.value().ok_or_else(|| {
797        TestScriptError::ExecutionError(format!(
798            "Failed to retrieve TestScript operation at '{}'.",
799            pointer.path()
800        ))
801    })
802}
803
804fn get_operation_type(operation: &TestScriptSetupActionOperation) -> Option<&str> {
805    operation
806        .type_
807        .as_ref()
808        .and_then(|t| t.code.as_ref())
809        .and_then(|c| c.value.as_deref())
810}
811
812fn require_target_id<'a>(
813    operation: &'a TestScriptSetupActionOperation,
814    path: &str,
815    operation_name: &str,
816) -> Result<&'a str, TestScriptError> {
817    operation
818        .targetId
819        .as_ref()
820        .and_then(|id| id.value.as_deref())
821        .ok_or_else(|| {
822            TestScriptError::ExecutionError(format!(
823                "{operation_name} operation requires targetId at '{path}'."
824            ))
825        })
826}
827
828fn require_source_id<'a>(
829    operation: &'a TestScriptSetupActionOperation,
830    path: &str,
831    operation_name: &str,
832) -> Result<&'a str, TestScriptError> {
833    operation
834        .sourceId
835        .as_ref()
836        .and_then(|id| id.value.as_deref())
837        .ok_or_else(|| {
838            TestScriptError::ExecutionError(format!(
839                "{operation_name} operation requires sourceId at '{path}'."
840            ))
841        })
842}
843
844fn fixture_string_field(
845    fixture: &dyn MetaValue,
846    fixture_name: &str,
847    field: &str,
848) -> Result<String, TestScriptError> {
849    fixture
850        .get_field(field)
851        .ok_or_else(|| {
852            TestScriptError::ExecutionError(format!(
853                "Fixture '{fixture_name}' does not have '{field}' field."
854            ))
855        })?
856        .as_any()
857        .downcast_ref::<String>()
858        .cloned()
859        .ok_or_else(|| {
860            TestScriptError::ExecutionError(format!(
861                "Field '{field}' on fixture '{fixture_name}' is not a String."
862            ))
863        })
864}
865
866fn fixture_version_id(
867    fixture: &dyn MetaValue,
868    fixture_name: &str,
869) -> Result<String, TestScriptError> {
870    fixture
871        .get_field("meta")
872        .and_then(|meta| meta.get_field("versionId"))
873        .ok_or_else(|| {
874            TestScriptError::ExecutionError(format!(
875                "Fixture '{fixture_name}' does not have a 'versionId' field."
876            ))
877        })?
878        .as_any()
879        .downcast_ref::<Box<FHIRId>>()
880        .cloned()
881        .and_then(|v| v.value)
882        .ok_or_else(|| {
883            TestScriptError::ExecutionError(format!(
884                "Fixture '{fixture_name}' does not have a valid 'versionId' field."
885            ))
886        })
887}
888
889fn fixture_resource(
890    fixture: &dyn MetaValue,
891    fixture_name: &str,
892) -> Result<Resource, TestScriptError> {
893    (fixture as &dyn Any)
894        .downcast_ref::<Resource>()
895        .cloned()
896        .ok_or_else(|| {
897            TestScriptError::ExecutionError(format!("Fixture '{fixture_name}' is not a Resource."))
898        })
899}
900
901async fn parsed_parameters(
902    state: &TestState,
903    root: TypedPointer<TestScript, TestScript>,
904    raw: &str,
905    path: &str,
906    operation: &str,
907) -> Result<ParsedParameters, TestScriptError> {
908    let evaluated = evaluate_variable(state, root, raw).await?;
909
910    ParsedParameters::try_from(evaluated.as_str()).map_err(|e| {
911        TestScriptError::ExecutionError(format!(
912            "Failed to parse parameters for {operation} operation at '{path}': {e}"
913        ))
914    })
915}
916
917/// Builds the headers declared by an operation's `requestHeader` entries.
918///
919/// Values are variable-evaluated first, so a `TestScript` may parameterize a header
920/// the same way it parameterizes a URL or a search query.
921async fn testscript_request_headers(
922    state: &TestState,
923    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
924    operation: &TestScriptSetupActionOperation,
925) -> Result<Option<HeaderMap>, TestScriptError> {
926    let Some(request_headers) = operation.requestHeader.as_ref() else {
927        return Ok(None);
928    };
929
930    let mut headers = HeaderMap::with_capacity(request_headers.len());
931
932    for request_header in request_headers {
933        let field = request_header.field.value.as_deref().ok_or_else(|| {
934            TestScriptError::ExecutionError(format!(
935                "Missing requestHeader field for operation at '{}'.",
936                pointer.path()
937            ))
938        })?;
939
940        let raw_value = request_header.value.value.as_deref().unwrap_or_default();
941        let value = evaluate_variable(state, pointer.root(), raw_value).await?;
942
943        let name = HeaderName::try_from(field).map_err(|e| {
944            TestScriptError::ExecutionError(format!(
945                "Invalid requestHeader field '{field}' for operation at '{}': {e}",
946                pointer.path()
947            ))
948        })?;
949
950        let value = HeaderValue::from_str(&value).map_err(|e| {
951            TestScriptError::ExecutionError(format!(
952                "Invalid requestHeader value for '{field}' for operation at '{}': {e}",
953                pointer.path()
954            ))
955        })?;
956
957        headers.insert(name, value);
958    }
959
960    Ok((!headers.is_empty()).then_some(headers))
961}
962
963async fn run_operation<CTX: WithRequestHeaders, Client: FHIRClient<CTX, OperationOutcomeError>>(
964    client: &Client,
965    ctx: CTX,
966    state: Arc<Mutex<TestState>>,
967    pointer: TypedPointer<TestScript, TestScriptSetupActionOperation>,
968    options: Arc<TestRunnerOptions>,
969) -> Result<TestResult<TestReportSetupActionOperation>, TestScriptError> {
970    let operation = pointer.value().ok_or_else(|| {
971        TestScriptError::ExecutionError(format!(
972            "Failed to retrieve TestScript operation at '{}'.",
973            pointer.path()
974        ))
975    })?;
976
977    let mut state_guard = state.lock().await;
978    let fhir_request = testscript_operation_to_fhir_request(&state_guard, &pointer).await?;
979
980    let ctx = match testscript_request_headers(&state_guard, &pointer, operation).await? {
981        Some(headers) => ctx.with_request_headers(headers),
982        None => ctx,
983    };
984
985    let fhir_response = client.request(ctx, fhir_request.clone()).await;
986    if let Some(wait_duration) = options.wait_between_operations {
987        tokio::time::sleep(wait_duration).await;
988    }
989
990    match fhir_response {
991        Ok(fhir_response) => {
992            associate_request_response_variables(
993                &mut state_guard,
994                operation,
995                fhir_request,
996                Response::FHIRResponse(Box::new(fhir_response)),
997            );
998
999            drop(state_guard);
1000
1001            Ok(TestResult {
1002                state: state.clone(),
1003                value: TestReportSetupActionOperation {
1004                    result: ReportActionResultCodes::pass(),
1005                    ..Default::default()
1006                },
1007            })
1008        }
1009        Err(op_error) => {
1010            let op_error = Arc::new(op_error);
1011            tracing::warn!(
1012                path = pointer.path(),
1013                operation.label = operation
1014                    .label
1015                    .as_ref()
1016                    .and_then(|l| l.value.as_deref())
1017                    .unwrap_or("<no-label>"),
1018                operation.operation_type = get_operation_type(operation).unwrap_or("<unknown>"),
1019                error = %op_error,
1020                "TestScript operation failed"
1021            );
1022            associate_request_response_variables(
1023                &mut state_guard,
1024                operation,
1025                fhir_request,
1026                Response::OperationError(op_error.clone()),
1027            );
1028
1029            Ok(TestResult {
1030                state: state.clone(),
1031                value: TestReportSetupActionOperation {
1032                    result: ReportActionResultCodes::warning(),
1033                    message: Some(Box::new(FHIRMarkdown {
1034                        value: Some(format!("Operation failed: {op_error}")),
1035                        ..Default::default()
1036                    })),
1037                    ..Default::default()
1038                },
1039            })
1040        }
1041    }
1042}
1043
1044fn get_source<'a>(
1045    state: &'a TestState,
1046    assertion: &TestScriptSetupActionAssert,
1047) -> Result<Option<&'a dyn MetaValue>, TestScriptError> {
1048    if let Some(source_id) = assertion.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
1049        let source = state.resolve_fixture(source_id)?;
1050        Ok(Some(source))
1051    } else {
1052        match assertion
1053            .direction
1054            .as_ref()
1055            .unwrap_or(&AssertDirectionCodes::response())
1056        {
1057            assertion if assertion == &AssertDirectionCodes::request() => {
1058                if let Some(request) = state.latest_request.as_ref() {
1059                    request_to_meta_value(request)
1060                        .ok_or_else(|| TestScriptError::InvalidFixture)
1061                        .map(Some)
1062                } else {
1063                    Ok(None)
1064                }
1065            }
1066            assertion if assertion == &AssertDirectionCodes::response() => {
1067                if let Some(response) = state.latest_response.as_ref() {
1068                    response_to_meta_value(response)
1069                        .ok_or_else(|| TestScriptError::InvalidFixture)
1070                        .map(Some)
1071                } else {
1072                    Ok(None)
1073                }
1074            }
1075            _ => Err(TestScriptError::ExecutionError(
1076                "Assert direction cannot be 'null' when sourceId is not provided.".to_string(),
1077            )),
1078        }
1079    }
1080}
1081
1082fn evaluate_operator(
1083    operator: &BoundCode<AssertOperatorCodes>,
1084    a: &Vec<conversion::ConvertedValue>,
1085    b: &Vec<conversion::ConvertedValue>,
1086) -> bool {
1087    match operator {
1088        operator
1089            if operator == &AssertOperatorCodes::equals()
1090                || operator == &AssertOperatorCodes::null() =>
1091        {
1092            a == b
1093        }
1094        operator if operator == &AssertOperatorCodes::not_equals() => !(a == b),
1095
1096        operator if operator == &AssertOperatorCodes::contains() => {
1097            if a.len() != 1 || b.len() != 1 {
1098                return false;
1099            }
1100
1101            match (&a[0], &b[0]) {
1102                (ConvertedValue::String(a_str), ConvertedValue::String(b_str)) => {
1103                    a_str.contains(b_str)
1104                }
1105                _ => false,
1106            }
1107        }
1108        operator if operator == &AssertOperatorCodes::empty() => {
1109            todo!("Empty operator not implemented")
1110        }
1111        operator if operator == &AssertOperatorCodes::eval() => {
1112            todo!("Eval operator not implemented")
1113        }
1114        operator if operator == &AssertOperatorCodes::greater_than() => {
1115            todo!("GreaterThan operator not implemented")
1116        }
1117        operator if operator == &AssertOperatorCodes::in_() => todo!("In operator not implemented"),
1118        operator if operator == &AssertOperatorCodes::less_than() => {
1119            todo!("LessThan operator not implemented")
1120        }
1121        operator if operator == &AssertOperatorCodes::not_contains() => {
1122            todo!("NotContains operator not implemented")
1123        }
1124        operator if operator == &AssertOperatorCodes::not_empty() => {
1125            todo!("NotEmpty operator not implemented")
1126        }
1127        operator if operator == &AssertOperatorCodes::not_in() => {
1128            todo!("NotIn operator not implemented")
1129        }
1130        _ => {
1131            todo!("Operator '{:?}' not implemented", operator)
1132        }
1133    }
1134    // a == b
1135}
1136
1137async fn derive_comparison_to(
1138    state: &TestState,
1139    assertion: &TestScriptSetupActionAssert,
1140) -> Result<Vec<ConvertedValue>, TestScriptError> {
1141    if let Some(comparision_fixture_id) = assertion
1142        .compareToSourceId
1143        .as_ref()
1144        .and_then(|c| c.value.as_ref())
1145    {
1146        let comparison_fixture = state.resolve_fixture(comparision_fixture_id)?;
1147
1148        let Some(comparison_expression) = assertion
1149            .compareToSourceExpression
1150            .as_ref()
1151            .and_then(|exp| exp.value.as_ref())
1152        else {
1153            return Err(TestScriptError::ExecutionError(
1154                "compareToSourceExpression is required when compareToSourceId is provided."
1155                    .to_string(),
1156            ));
1157        };
1158
1159        let result = state
1160            .fp_engine
1161            .evaluate(comparison_expression, vec![comparison_fixture])
1162            .await
1163            .map_err(|e| {
1164                TestScriptError::ExecutionError(format!(
1165                    "FHIRPath evaluation error for comparison fixture '{comparision_fixture_id}': {e}"
1166                ))
1167            })?;
1168
1169        Ok(result
1170            .iter()
1171            .map(conversion::convert_meta_value)
1172            .collect::<Vec<_>>())
1173    } else if let Some(value) = assertion.value.as_ref().and_then(|v| v.value.as_ref())
1174        && let Some(converted_value) = conversion::convert_string_value(value.as_ref())
1175    {
1176        Ok(vec![converted_value])
1177    } else {
1178        Err(TestScriptError::ExecutionError(
1179            "Failed to derive comparison value for assertion.".to_string(),
1180        ))
1181    }
1182}
1183
1184/// Assertions carry a `label`/`description` intended by the FHIR spec for
1185/// exactly this purpose: identifying an assertion in test engine output.
1186fn assert_label(assertion: &TestScriptSetupActionAssert) -> &str {
1187    assertion
1188        .label
1189        .as_ref()
1190        .and_then(|l| l.value.as_deref())
1191        .or_else(|| {
1192            assertion
1193                .description
1194                .as_ref()
1195                .and_then(|d| d.value.as_deref())
1196        })
1197        .unwrap_or("<no-label>")
1198}
1199
1200/// Assertions are what determine the testreports ultimate pass/fail status.
1201/// So set that within state here depending on assertion success/failure.
1202async fn run_assertion(
1203    state: Arc<Mutex<TestState>>,
1204    pointer: TypedPointer<TestScript, TestScriptSetupActionAssert>,
1205) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
1206    let assertion = pointer.value().ok_or_else(|| {
1207        TestScriptError::ExecutionError(format!(
1208            "Failed to retrieve TestScript assertion at '{}'.",
1209            pointer.path()
1210        ))
1211    })?;
1212
1213    let mut state_guard = state.lock().await;
1214
1215    let Some(source) = get_source(&state_guard, assertion)? else {
1216        return Err(TestScriptError::ExecutionError(format!(
1217            "Failed to resolve source for assertion at '{}'.",
1218            pointer.path()
1219        )));
1220    };
1221
1222    let default = AssertOperatorCodes::equals();
1223    let operator = assertion.operator.as_ref().unwrap_or(&default);
1224
1225    // Keep all uses of `source` inside this scope so the immutable borrow
1226    // of `state_guard` ends before we mutate `state_guard`.
1227    let failure_message = if let Some(message) =
1228        evaluate_resource_assertion(assertion, pointer.path(), source, operator)
1229    {
1230        Some(message)
1231    } else if let Some(expression) = assertion.expression.as_ref().and_then(|e| e.value.as_ref()) {
1232        evaluate_expression_assertion(
1233            &state_guard,
1234            assertion,
1235            pointer.path(),
1236            source,
1237            operator,
1238            expression,
1239        )
1240        .await?
1241    } else {
1242        None
1243    };
1244
1245    let (result, message) = match failure_message {
1246        Some(message) => {
1247            tracing::error!(
1248                assert.label = assert_label(assertion),
1249                path = pointer.path(),
1250                "{message}"
1251            );
1252
1253            state_guard.result = ReportResultCodes::fail();
1254
1255            (
1256                ReportActionResultCodes::fail(),
1257                Some(Box::new(FHIRMarkdown {
1258                    value: Some(message),
1259                    ..Default::default()
1260                })),
1261            )
1262        }
1263        None => (ReportActionResultCodes::pass(), None),
1264    };
1265
1266    Ok(TestResult {
1267        state: state.clone(),
1268        value: TestReportSetupActionAssert {
1269            result,
1270            message,
1271            ..Default::default()
1272        },
1273    })
1274}
1275
1276fn evaluate_resource_assertion(
1277    assertion: &TestScriptSetupActionAssert,
1278    path: &str,
1279    source: &dyn MetaValue,
1280    operator: &BoundCode<AssertOperatorCodes>,
1281) -> Option<String> {
1282    let resource_string = assertion.resource.as_ref()?.as_str()?;
1283
1284    let operation_evaluation_result = evaluate_operator(
1285        operator,
1286        &vec![conversion::ConvertedValue::String(
1287            resource_string.to_string(),
1288        )],
1289        &vec![conversion::ConvertedValue::String(
1290            source.fhir_type().to_string(),
1291        )],
1292    );
1293
1294    if operation_evaluation_result {
1295        return None;
1296    }
1297
1298    Some(format!(
1299        "Assertion '{}' at '{}' failed: resource type '{resource_string}' does not match '{}'.",
1300        assert_label(assertion),
1301        path,
1302        source.fhir_type()
1303    ))
1304}
1305
1306async fn evaluate_expression_assertion(
1307    state_guard: &TestState,
1308    assertion: &TestScriptSetupActionAssert,
1309    path: &str,
1310    source: &dyn MetaValue,
1311    operator: &BoundCode<AssertOperatorCodes>,
1312    expression: &str,
1313) -> Result<Option<String>, TestScriptError> {
1314    let comparison_to = derive_comparison_to(state_guard, assertion).await?;
1315
1316    let Ok(result) = state_guard
1317        .fp_engine
1318        .evaluate(expression, vec![source])
1319        .await
1320    else {
1321        tracing::error!(
1322            assert.label = assert_label(assertion),
1323            path = path,
1324            expression = expression,
1325            "Assertion '{}' at '{}' failed: FHIRPath expression '{expression}' failed to evaluate.",
1326            assert_label(assertion),
1327            path,
1328        );
1329
1330        return Err(TestScriptError::ExecutionError(format!(
1331            "Assertion '{}' at '{}': FHIRPath expression '{expression}' failed to evaluate.",
1332            assert_label(assertion),
1333            path,
1334        )));
1335    };
1336
1337    let converted_values = result
1338        .iter()
1339        .map(conversion::convert_meta_value)
1340        .collect::<Vec<_>>();
1341
1342    let operation_evaluation_result =
1343        evaluate_operator(operator, &converted_values, &comparison_to);
1344
1345    if operation_evaluation_result {
1346        return Ok(None);
1347    }
1348
1349    Ok(Some(format!(
1350        "Assertion '{}' at '{}' failed: '{converted_values:?}' {operator:?} '{comparison_to:?}'.",
1351        assert_label(assertion),
1352        path,
1353    )))
1354}
1355
1356async fn run_action<CTX: WithRequestHeaders, Client: FHIRClient<CTX, OperationOutcomeError>>(
1357    client: &Client,
1358    ctx: CTX,
1359    state: Arc<Mutex<TestState>>,
1360    pointer: TypedPointer<TestScript, TestScriptTestAction>,
1361    options: Arc<TestRunnerOptions>,
1362) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1363    tracing::info!("Running TestScript action at path: {}", pointer.path());
1364    let action = pointer.value().ok_or_else(|| {
1365        TestScriptError::ExecutionError(format!(
1366            "Failed to retrieve TestScript action at '{}'.",
1367            pointer.path()
1368        ))
1369    })?;
1370
1371    // Should be either an operation or an assert.
1372    // Both should not exist at the same time.
1373    if action.operation.is_some() {
1374        let Some(operation_pointer) =
1375            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1376        else {
1377            return Err(TestScriptError::ExecutionError(format!(
1378                "Failed to retrieve TestScript operation at '{}'.",
1379                pointer.path()
1380            )));
1381        };
1382
1383        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1384
1385        Ok(TestResult {
1386            state: result.state,
1387            value: TestReportSetupAction {
1388                operation: Some(result.value),
1389                ..Default::default()
1390            },
1391        })
1392    } else if action.assert.is_some() {
1393        let Some(assertion_pointer) =
1394            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1395        else {
1396            return Err(TestScriptError::ExecutionError(format!(
1397                "Failed to retrieve TestScript assertion at '{}'.",
1398                pointer.path()
1399            )));
1400        };
1401
1402        let assertion = run_assertion(state, assertion_pointer).await?;
1403
1404        Ok(TestResult {
1405            state: assertion.state,
1406            value: TestReportSetupAction {
1407                assert: Some(assertion.value),
1408                ..Default::default()
1409            },
1410        })
1411    } else {
1412        Err(TestScriptError::ExecutionError(format!(
1413            "TestScript action must have either an operation or an assert at '{}'.",
1414            pointer.path()
1415        )))
1416    }
1417}
1418
1419async fn run_setup_action<
1420    CTX: WithRequestHeaders,
1421    Client: FHIRClient<CTX, OperationOutcomeError>,
1422>(
1423    client: &Client,
1424    ctx: CTX,
1425    state: Arc<Mutex<TestState>>,
1426    pointer: TypedPointer<TestScript, TestScriptSetupAction>,
1427    options: Arc<TestRunnerOptions>,
1428) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1429    let action = pointer.value().ok_or_else(|| {
1430        TestScriptError::ExecutionError(format!(
1431            "Failed to retrieve TestScript action at '{}'.",
1432            pointer.path()
1433        ))
1434    })?;
1435
1436    tracing::info!("Running TestScript action at path: {}", pointer.path());
1437
1438    // Should be either an operation or an assert.
1439    // Both should not exist at the same time.
1440    if action.operation.is_some() {
1441        let Some(operation_pointer) =
1442            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1443        else {
1444            return Err(TestScriptError::ExecutionError(format!(
1445                "Failed to retrieve TestScript operation at '{}'.",
1446                pointer.path()
1447            )));
1448        };
1449
1450        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1451
1452        Ok(TestResult {
1453            state: result.state,
1454            value: TestReportSetupAction {
1455                operation: Some(result.value),
1456                ..Default::default()
1457            },
1458        })
1459    } else if action.assert.is_some() {
1460        let Some(assertion_pointer) =
1461            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1462        else {
1463            return Err(TestScriptError::ExecutionError(format!(
1464                "Failed to retrieve TestScript assertion at '{}'.",
1465                pointer.path()
1466            )));
1467        };
1468
1469        let assertion = run_assertion(state, assertion_pointer).await?;
1470
1471        Ok(TestResult {
1472            state: assertion.state,
1473            value: TestReportSetupAction {
1474                assert: Some(assertion.value),
1475                ..Default::default()
1476            },
1477        })
1478    } else {
1479        Err(TestScriptError::ExecutionError(format!(
1480            "TestScript action must have either an operation or an assert at '{}'.",
1481            pointer.path()
1482        )))
1483    }
1484}
1485
1486async fn setup_fixtures<
1487    CTX: Clone + WithRequestHeaders,
1488    Client: FHIRClient<CTX, OperationOutcomeError>,
1489>(
1490    client: &Client,
1491    ctx: CTX,
1492    state: Arc<Mutex<TestState>>,
1493    pointer: TypedPointer<TestScript, TestScript>,
1494    _options: Arc<TestRunnerOptions>,
1495) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
1496    let mut state_lock = state.lock().await;
1497
1498    let Some(fixtures_pointer) =
1499        pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
1500    else {
1501        return Ok(state.clone());
1502    };
1503
1504    let Some(fixtures) = fixtures_pointer.value() else {
1505        return Ok(state.clone());
1506    };
1507
1508    for fixture in fixtures {
1509        if let Some(reference_string) = fixture
1510            .resource
1511            .as_ref()
1512            .and_then(|r| r.reference.as_ref())
1513            .and_then(|refe| refe.value.as_ref())
1514        {
1515            let resolved_resource = if reference_string.starts_with('#')
1516                && let Some(contained) =
1517                    pointer.descend::<Vec<Resource>>(&Key::Field("contained".to_string()))
1518                && let Some(contained) = contained.value()
1519            {
1520                let local_id = &reference_string[1..];
1521                let Some(resource) = contained.iter().find(|res| {
1522                    if let Some(id) = res.get_field("id")
1523                        && let Some(id) = id.as_any().downcast_ref::<String>()
1524                    {
1525                        id.as_str() == local_id
1526                    } else {
1527                        false
1528                    }
1529                }) else {
1530                    return Err(OperationOutcomeError::error(
1531                        IssueType::not_found(),
1532                        format!("Contained resource with id '{local_id}' not found."),
1533                    ));
1534                };
1535
1536                resource.clone()
1537            } else {
1538                let parts = reference_string.split('/').collect::<Vec<&str>>();
1539                if parts.len() != 2 {
1540                    return Err(OperationOutcomeError::error(
1541                        IssueType::invalid(),
1542                        format!("Invalid fixture reference: {reference_string}"),
1543                    ));
1544                }
1545
1546                let resource_type = parts[0];
1547                let id = parts[1];
1548
1549                let Some(remote_resource) = client
1550                    .read(
1551                        ctx.clone(),
1552                        ResourceType::try_from(resource_type).map_err(|_| {
1553                            OperationOutcomeError::error(
1554                                IssueType::invalid(),
1555                                format!(
1556                                    "Invalid resource type in fixture reference: '{resource_type}'"
1557                                ),
1558                            )
1559                        })?,
1560                        id.to_string(),
1561                    )
1562                    .await?
1563                else {
1564                    return Err(OperationOutcomeError::error(
1565                        IssueType::not_found(),
1566                        format!("Resource '{resource_type}' with id '{id}' not found."),
1567                    ));
1568                };
1569
1570                remote_resource
1571            };
1572
1573            state_lock.fixtures.insert(
1574                fixture.id.clone().unwrap_or_default(),
1575                Fixtures::Resource(resolved_resource),
1576            );
1577        }
1578    }
1579
1580    drop(state_lock);
1581
1582    Ok(state)
1583}
1584
1585async fn run_setup<
1586    CTX: Clone + WithRequestHeaders,
1587    Client: FHIRClient<CTX, OperationOutcomeError>,
1588>(
1589    client: &Client,
1590    ctx: CTX,
1591    state: Arc<Mutex<TestState>>,
1592    pointer: TypedPointer<TestScript, TestScriptSetup>,
1593    options: Arc<TestRunnerOptions>,
1594) -> Result<TestResult<TestReportSetup>, TestScriptError> {
1595    let mut cur_state = state;
1596
1597    let mut setup_results = TestReportSetup {
1598        action: vec![],
1599        ..Default::default()
1600    };
1601
1602    let Some(setup) = pointer.value() else {
1603        return Ok(TestResult {
1604            state: cur_state,
1605            value: setup_results,
1606        });
1607    };
1608
1609    for action in setup.action.iter().enumerate() {
1610        let action_pointer = pointer
1611            .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
1612            .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
1613
1614        let action_pointer = action_pointer.ok_or_else(|| {
1615            TestScriptError::ExecutionError(format!(
1616                "Failed to retrieve TestScript action at index {}.",
1617                action.0
1618            ))
1619        })?;
1620
1621        let result = run_setup_action(
1622            client,
1623            ctx.clone(),
1624            cur_state,
1625            action_pointer,
1626            options.clone(),
1627        )
1628        .await?;
1629        cur_state = result.state;
1630
1631        setup_results.action.push(result.value);
1632    }
1633
1634    Ok(TestResult {
1635        state: cur_state,
1636        value: setup_results,
1637    })
1638}
1639
1640async fn run_teardown<
1641    CTX: Clone + WithRequestHeaders,
1642    Client: FHIRClient<CTX, OperationOutcomeError>,
1643>(
1644    client: &Client,
1645    ctx: CTX,
1646    state: Arc<Mutex<TestState>>,
1647    pointer: TypedPointer<TestScript, TestScriptTeardown>,
1648    options: Arc<TestRunnerOptions>,
1649) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
1650    let mut cur_state = state;
1651
1652    let mut teardown_results = TestReportTeardown {
1653        action: vec![],
1654        ..Default::default()
1655    };
1656
1657    let Some(actions) = pointer.value() else {
1658        return Ok(TestResult {
1659            state: cur_state,
1660            value: teardown_results,
1661        });
1662    };
1663
1664    for action in actions.action.iter().enumerate() {
1665        let action_pointer = pointer
1666            .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
1667            .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
1668
1669        let action_pointer = action_pointer.ok_or_else(|| {
1670            TestScriptError::ExecutionError(format!(
1671                "Failed to retrieve TestScript teardown action at index {}.",
1672                action.0
1673            ))
1674        })?;
1675
1676        let operation_pointer = action_pointer
1677            .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1678            .ok_or_else(|| {
1679                TestScriptError::ExecutionError(format!(
1680                    "Failed to retrieve TestScript teardown operation at index {}.",
1681                    action.0
1682                ))
1683            })?;
1684
1685        let result = run_operation(
1686            client,
1687            ctx.clone(),
1688            cur_state,
1689            operation_pointer,
1690            options.clone(),
1691        )
1692        .await?;
1693        cur_state = result.state;
1694
1695        teardown_results.action.push(TestReportTeardownAction {
1696            operation: result.value,
1697            ..Default::default()
1698        });
1699    }
1700
1701    Ok(TestResult {
1702        state: cur_state,
1703        value: teardown_results,
1704    })
1705}
1706
1707async fn run_test<
1708    CTX: Clone + WithRequestHeaders,
1709    Client: FHIRClient<CTX, OperationOutcomeError>,
1710>(
1711    client: &Client,
1712    ctx: CTX,
1713    state: Arc<Mutex<TestState>>,
1714    pointer: TypedPointer<TestScript, TestScriptTest>,
1715    options: Arc<TestRunnerOptions>,
1716) -> Result<TestResult<TestReportTest>, TestScriptError> {
1717    let mut cur_state = state;
1718    let mut test_report_test = TestReportTest {
1719        action: vec![],
1720        ..Default::default()
1721    };
1722
1723    let test = pointer.value().ok_or_else(|| {
1724        TestScriptError::ExecutionError(format!(
1725            "Failed to retrieve TestScript test at '{}'.",
1726            pointer.path()
1727        ))
1728    })?;
1729
1730    for action in test.action.iter().enumerate() {
1731        let Some(action_pointer) = pointer
1732            .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
1733            .and_then(|p| p.descend(&Key::Index(action.0)))
1734        else {
1735            return Err(TestScriptError::ExecutionError(format!(
1736                "Failed to retrieve TestScript test action at index {}.",
1737                action.0
1738            )));
1739        };
1740        let result = run_action(
1741            client,
1742            ctx.clone(),
1743            cur_state,
1744            action_pointer,
1745            options.clone(),
1746        )
1747        .await?;
1748        cur_state = result.state;
1749        test_report_test.action.push(TestReportTestAction {
1750            operation: result.value.operation,
1751            assert: result.value.assert,
1752            ..Default::default()
1753        });
1754    }
1755
1756    Ok(TestResult {
1757        state: cur_state,
1758        value: test_report_test,
1759    })
1760}
1761
1762async fn run_tests<
1763    CTX: Clone + WithRequestHeaders,
1764    Client: FHIRClient<CTX, OperationOutcomeError>,
1765>(
1766    client: &Client,
1767    ctx: CTX,
1768    state: Arc<Mutex<TestState>>,
1769    pointer: TypedPointer<TestScript, Vec<TestScriptTest>>,
1770    options: Arc<TestRunnerOptions>,
1771) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
1772    let mut test_results = vec![];
1773    let mut cur_state = state;
1774
1775    let Some(tests) = pointer.value() else {
1776        return Ok(TestResult {
1777            state: cur_state,
1778            value: test_results,
1779        });
1780    };
1781
1782    for test in tests.iter().enumerate() {
1783        let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
1784            return Err(TestScriptError::ExecutionError(format!(
1785                "Failed to retrieve TestScript test at index {}.",
1786                test.0
1787            )));
1788        };
1789        let test_result = run_test(
1790            client,
1791            ctx.clone(),
1792            cur_state,
1793            test_pointer,
1794            options.clone(),
1795        )
1796        .await?;
1797        cur_state = test_result.state;
1798        test_results.push(test_result.value);
1799    }
1800
1801    Ok(TestResult {
1802        state: cur_state,
1803        value: test_results,
1804    })
1805}
1806
1807pub struct TestRunnerOptions {
1808    pub wait_between_operations: Option<Duration>,
1809}
1810
1811/// Runs a FHIR `TestScript` using the provided client and execution context.
1812///
1813/// This executes the `TestScript` lifecycle:
1814/// - fixture setup
1815/// - setup actions
1816/// - test actions
1817/// - teardown actions
1818///
1819/// # Errors
1820///
1821/// Returns [`TestScriptError`] if:
1822/// - fixture setup fails
1823/// - setup actions fail
1824/// - test execution fails
1825/// - teardown execution fails
1826/// - an operation performed by the FHIR client fails
1827#[tracing::instrument(
1828    name = "testscript_run",
1829    skip_all,
1830    fields(
1831        testscript.id = test_script.id.as_deref().unwrap_or("<no-id>"),
1832        testscript.name = test_script.name.value.as_deref().unwrap_or("<unnamed>"),
1833        testscript.url = test_script.url.value.as_deref().unwrap_or("<no-url>"),
1834    )
1835)]
1836pub async fn run<
1837    CTX: Clone + WithRequestHeaders,
1838    Client: FHIRClient<CTX, OperationOutcomeError>,
1839>(
1840    client: &Client,
1841    ctx: CTX,
1842    test_script: Arc<TestScript>,
1843    options: Arc<TestRunnerOptions>,
1844) -> Result<TestReport, TestScriptError> {
1845    tracing::info!("Starting TestScript run");
1846
1847    let mut test_report = TestReport {
1848        status: ReportStatusCodes::completed(),
1849        testScript: Box::new(Reference {
1850            reference: Some(Box::new(FHIRString {
1851                value: Some(format!(
1852                    "Testscript/{}",
1853                    test_script.id.clone().unwrap_or_default()
1854                )),
1855                ..Default::default()
1856            })),
1857            ..Default::default()
1858        }),
1859        ..Default::default()
1860    };
1861
1862    let mut state = Arc::new(Mutex::new(TestState::new()));
1863    let pointer = TypedPointer::<TestScript, TestScript>::new(test_script);
1864
1865    state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
1866        .await
1867        .map_err(TestScriptError::OperationError)?;
1868
1869    let mut running_state = Ok(());
1870
1871    // Run setup actions
1872    if let Some(setup_pointer) =
1873        pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
1874    {
1875        tracing::info!("Running TestScript setup...");
1876        let setup_result = run_setup(
1877            client,
1878            ctx.clone(),
1879            state.clone(),
1880            setup_pointer,
1881            options.clone(),
1882        )
1883        .await;
1884        match setup_result {
1885            Ok(res) => {
1886                state = res.state;
1887                test_report.setup = Some(res.value);
1888            }
1889            Err(e) => {
1890                tracing::error!(phase = "setup", error = ?e, "TestScript run failed during setup");
1891                running_state = Err(e);
1892            }
1893        }
1894    }
1895
1896    // Run Test actions
1897    if running_state.is_ok()
1898        && let Some(test_pointer) =
1899            pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
1900    {
1901        tracing::info!("Running TestScript tests...");
1902        let test_result = run_tests(
1903            client,
1904            ctx.clone(),
1905            state.clone(),
1906            test_pointer,
1907            options.clone(),
1908        )
1909        .await;
1910
1911        match test_result {
1912            Ok(res) => {
1913                state = res.state;
1914                test_report.test = Some(res.value);
1915            }
1916
1917            Err(e) => {
1918                tracing::error!(phase = "test", error = ?e, "TestScript run failed during test execution");
1919                running_state = Err(e);
1920            }
1921        }
1922    }
1923
1924    if let Some(teardown_pointer) =
1925        pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
1926    {
1927        tracing::info!("Running TestScript teardown...");
1928
1929        let result = run_teardown(
1930            client,
1931            ctx.clone(),
1932            state.clone(),
1933            teardown_pointer,
1934            options.clone(),
1935        )
1936        .await?;
1937
1938        // state = result.state;
1939        test_report.teardown = Some(result.value);
1940    }
1941
1942    running_state?;
1943
1944    let state_guard = state.lock().await;
1945    // Only set result to fail so if still pending can assume pass.
1946    // Flip to fail in assertion tests if any fail.
1947    match &state_guard.result {
1948        state if state == &ReportResultCodes::pending() => {
1949            test_report.result = ReportResultCodes::pass();
1950        }
1951        status => test_report.result = status.clone(),
1952    }
1953
1954    if test_report.result == ReportResultCodes::fail() {
1955        tracing::error!(result = ?test_report.result, "TestScript run finished with failures");
1956    } else {
1957        tracing::info!(result = ?test_report.result, "TestScript run finished");
1958    }
1959
1960    Ok(test_report)
1961}