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