Skip to main content

haste_health/commands/
api.rs

1use crate::cli::state::CliState;
2use clap::Subcommand;
3use haste_fhir_client::{FHIRClient, url::ParsedParameters};
4use haste_fhir_model::r4::generated::{
5    resources::{Bundle, Resource, ResourceType},
6    terminology::IssueType,
7};
8use haste_fhir_operation_error::OperationOutcomeError;
9use std::sync::Arc;
10use tokio::sync::Mutex;
11
12/// Make FHIR REST API calls against the active profile's server.
13///
14/// Commands that take a resource body accept it via `--data` (inline JSON), `--file`
15/// (a JSON file), or, if neither is given, a single line read from stdin.
16#[derive(Subcommand, Debug)]
17pub(crate) enum ApiCommands {
18    /// Create a resource (`POST [base]/[type]`).
19    Create {
20        /// Resource JSON, inline.
21        #[arg(short, long)]
22        data: Option<String>,
23        /// Path to a file containing the resource JSON.
24        #[arg(short, long)]
25        file: Option<String>,
26        /// FHIR resource type to create.
27        resource_type: String,
28    },
29    /// Read the current version of a resource (`GET [base]/[type]/[id]`).
30    Read { resource_type: String, id: String },
31
32    /// Read a specific historical version of a resource (`GET [base]/[type]/[id]/_history/[vid]`).
33    VersionRead {
34        resource_type: String,
35        id: String,
36        version_id: String,
37    },
38
39    /// Apply a JSON Patch to a resource (`PATCH [base]/[type]/[id]`).
40    Patch {
41        /// JSON Patch document, inline.
42        #[arg(short, long)]
43        data: Option<String>,
44        /// Path to a file containing the JSON Patch document.
45        #[arg(short, long)]
46        file: Option<String>,
47        resource_type: String,
48        id: String,
49    },
50    /// Create or replace a resource at a known ID (`PUT [base]/[type]/[id]`).
51    Update {
52        /// Resource JSON, inline.
53        #[arg(short, long)]
54        data: Option<String>,
55        /// Path to a file containing the resource JSON.
56        #[arg(short, long)]
57        file: Option<String>,
58        resource_type: String,
59        id: String,
60    },
61    /// Submit a transaction Bundle (`POST [base]`, type `transaction`).
62    Transaction {
63        /// Bundle JSON, inline.
64        #[arg(short, long)]
65        data: Option<String>,
66        /// Submit the same bundle this many times concurrently. Defaults to 1.
67        #[arg(short, long)]
68        parallel: Option<usize>,
69        /// Path to a file containing the Bundle JSON.
70        #[arg(short, long)]
71        file: Option<String>,
72        /// Print each response bundle to stdout.
73        #[arg(short, long)]
74        output: Option<bool>,
75    },
76    /// Submit a batch Bundle (`POST [base]`, type `batch`).
77    Batch {
78        /// Bundle JSON, inline.
79        #[arg(short, long)]
80        data: Option<String>,
81        /// Path to a file containing the Bundle JSON.
82        #[arg(short, long)]
83        file: Option<String>,
84        /// Print the response bundle to stdout.
85        #[arg(short, long)]
86        output: Option<bool>,
87    },
88
89    /// Fetch the system-wide history (`GET [base]/_history`).
90    HistorySystem {
91        /// FHIR search-style parameters, e.g. `_since=2024-01-01`.
92        parameters: Option<String>,
93    },
94
95    /// Fetch the history of a resource type (`GET [base]/[type]/_history`).
96    HistoryType {
97        resource_type: String,
98        /// FHIR search-style parameters, e.g. `_since=2024-01-01`.
99        parameters: Option<String>,
100    },
101
102    /// Fetch the history of a single resource instance (`GET [base]/[type]/[id]/_history`).
103    HistoryInstance {
104        resource_type: String,
105        id: String,
106        /// FHIR search-style parameters, e.g. `_since=2024-01-01`.
107        parameters: Option<String>,
108    },
109
110    /// Search a resource type (`GET [base]/[type]?...`).
111    SearchType {
112        resource_type: String,
113        /// FHIR search parameters, e.g. `name=eve&_count=20`.
114        parameters: Option<String>,
115    },
116
117    /// Search across all resource types (`GET [base]?...`).
118    SearchSystem {
119        /// FHIR search parameters, e.g. `_lastUpdated=gt2024-01-01`.
120        parameters: Option<String>,
121    },
122
123    /// Invoke a system-level operation (`POST [base]/$[operation_name]`).
124    InvokeSystem {
125        /// Parameters resource JSON, inline.
126        #[arg(short, long)]
127        data: Option<String>,
128        /// Path to a file containing the Parameters resource JSON.
129        #[arg(short, long)]
130        file: Option<String>,
131        operation_name: String,
132    },
133
134    /// Invoke a type-level operation (`POST [base]/[type]/$[operation_name]`).
135    InvokeType {
136        /// Parameters resource JSON, inline.
137        #[arg(short, long)]
138        data: Option<String>,
139        /// Path to a file containing the Parameters resource JSON.
140        #[arg(short, long)]
141        file: Option<String>,
142        resource_type: String,
143        operation_name: String,
144    },
145
146    /// Fetch the server's CapabilityStatement (`GET [base]/metadata`).
147    Capabilities {},
148
149    /// Delete a single resource instance (`DELETE [base]/[type]/[id]`).
150    DeleteInstance { resource_type: String, id: String },
151
152    /// Delete all resources of a type matching search parameters (`DELETE [base]/[type]?...`).
153    DeleteType {
154        resource_type: String,
155        /// FHIR search parameters selecting which resources to delete.
156        parameters: Option<String>,
157    },
158
159    /// Delete all resources matching system-level search parameters (`DELETE [base]?...`).
160    DeleteSystem {
161        /// FHIR search parameters selecting which resources to delete.
162        parameters: Option<String>,
163    },
164
165    /// Invoke an instance-level operation (`POST [base]/[type]/[id]/$[operation_name]`).
166    InvokeInstance {
167        /// Parameters resource JSON, inline.
168        #[arg(short, long)]
169        data: Option<String>,
170        /// Path to a file containing the Parameters resource JSON.
171        #[arg(short, long)]
172        file: Option<String>,
173        resource_type: String,
174        id: String,
175        operation_name: String,
176    },
177}
178
179async fn derive_resource_data_arg_file_arg_or_stdin<Type: serde::de::DeserializeOwned>(
180    data_arg: &Option<String>,
181    file_path: &Option<String>,
182) -> Result<Type, OperationOutcomeError> {
183    if let Some(data) = data_arg {
184        serde_json::from_str::<Type>(data).map_err(|e| {
185            OperationOutcomeError::error(
186                IssueType::exception(),
187                format!("Failed to parse transaction data: {}", e),
188            )
189        })
190    } else if let Some(file_path) = file_path {
191        let file_content = tokio::fs::read_to_string(file_path).await.map_err(|e| {
192            OperationOutcomeError::error(
193                IssueType::exception(),
194                format!("Failed to read transaction file: {}", e),
195            )
196        })?;
197
198        serde_json::from_str::<Type>(&file_content).map_err(|e| {
199            OperationOutcomeError::error(
200                IssueType::exception(),
201                format!("Failed to parse file: {}", e),
202            )
203        })
204    } else {
205        // Read from stdin
206        let mut buffer = String::new();
207
208        std::io::stdin().read_line(&mut buffer).map_err(|e| {
209            OperationOutcomeError::error(
210                IssueType::exception(),
211                format!("Failed to read from stdin: {}", e),
212            )
213        })?;
214
215        serde_json::from_str::<Type>(&buffer).map_err(|e| {
216            OperationOutcomeError::error(
217                IssueType::exception(),
218                format!("Failed to parse transaction from stdin: {}", e),
219            )
220        })
221    }
222}
223
224/// Runs the `api` command group.
225pub(crate) async fn run(
226    state: Arc<Mutex<CliState>>,
227    command: &ApiCommands,
228) -> Result<(), OperationOutcomeError> {
229    let fhir_client = crate::cli::client::fhir_client(state).await?;
230
231    match command {
232        ApiCommands::Create {
233            data,
234            resource_type,
235            file,
236        } => {
237            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
238                OperationOutcomeError::error(
239                    IssueType::invalid(),
240                    format!(
241                        "'{}' is not a valid FHIR resource type: {}",
242                        resource_type, e
243                    ),
244                )
245            })?;
246
247            let resource =
248                derive_resource_data_arg_file_arg_or_stdin::<Resource>(data, file).await?;
249
250            let result = fhir_client.create((), resource_type, resource).await?;
251
252            println!(
253                "{}",
254                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
255            );
256
257            Ok(())
258        }
259        ApiCommands::Read { resource_type, id } => {
260            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
261                OperationOutcomeError::error(
262                    IssueType::invalid(),
263                    format!(
264                        "'{}' is not a valid FHIR resource type: {}",
265                        resource_type, e
266                    ),
267                )
268            })?;
269
270            let result = fhir_client.read((), resource_type, id.clone()).await?;
271
272            println!(
273                "{}",
274                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
275            );
276
277            Ok(())
278        }
279        ApiCommands::Patch {
280            resource_type,
281            id,
282            data,
283            file,
284        } => {
285            let patches = if let Some(file) = file {
286                let file_content = tokio::fs::read_to_string(file).await.map_err(|e| {
287                    OperationOutcomeError::error(
288                        IssueType::exception(),
289                        format!("Failed to read transaction file: {}", e),
290                    )
291                })?;
292
293                serde_json::from_str::<json_patch::Patch>(&file_content).map_err(|e| {
294                    OperationOutcomeError::error(
295                        IssueType::invalid(),
296                        format!("Failed to parse patch JSON: {}", e),
297                    )
298                })?
299            } else if let Some(data) = data {
300                serde_json::from_str::<json_patch::Patch>(&data).map_err(|e| {
301                    OperationOutcomeError::error(
302                        IssueType::invalid(),
303                        format!("Failed to parse patch JSON: {}", e),
304                    )
305                })?
306            } else {
307                return Err(OperationOutcomeError::error(
308                    IssueType::invalid(),
309                    "Either --data or --file must be provided for patch operation.".to_string(),
310                ));
311            };
312
313            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
314                OperationOutcomeError::error(
315                    IssueType::invalid(),
316                    format!(
317                        "'{}' is not a valid FHIR resource type: {}",
318                        resource_type, e
319                    ),
320                )
321            })?;
322
323            let result = fhir_client
324                .patch((), resource_type, id.clone(), patches)
325                .await?;
326
327            println!(
328                "{}",
329                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
330            );
331
332            Ok(())
333        }
334        ApiCommands::Update {
335            resource_type,
336            id,
337            data,
338            file,
339        } => {
340            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
341                OperationOutcomeError::error(
342                    IssueType::invalid(),
343                    format!(
344                        "'{}' is not a valid FHIR resource type: {}",
345                        resource_type, e
346                    ),
347                )
348            })?;
349
350            let resource =
351                derive_resource_data_arg_file_arg_or_stdin::<Resource>(data, file).await?;
352
353            let result = fhir_client
354                .update((), resource_type, id.clone(), resource)
355                .await?;
356
357            println!(
358                "{}",
359                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
360            );
361            Ok(())
362        }
363        ApiCommands::Transaction {
364            data,
365            file,
366            output,
367            parallel,
368        } => {
369            let bundle = derive_resource_data_arg_file_arg_or_stdin::<Bundle>(data, file).await?;
370
371            let parallel = parallel.unwrap_or(1);
372
373            let mut futures = tokio::task::JoinSet::new();
374
375            for _ in 0..parallel {
376                let client = fhir_client.clone();
377                let bundle = bundle.clone();
378                let res = async move { client.transaction((), bundle).await };
379                futures.spawn(res);
380            }
381
382            let res = futures.join_all().await;
383
384            for bundle_result in res {
385                let bundle = bundle_result?;
386                if let Some(true) = output {
387                    println!(
388                        "{}",
389                        serde_json::to_string_pretty(&bundle)
390                            .expect("Failed to serialize response")
391                    );
392                }
393            }
394
395            Ok(())
396        }
397        ApiCommands::VersionRead {
398            resource_type,
399            id,
400            version_id,
401        } => {
402            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
403                OperationOutcomeError::error(
404                    IssueType::invalid(),
405                    format!(
406                        "'{}' is not a valid FHIR resource type: {}",
407                        resource_type, e
408                    ),
409                )
410            })?;
411
412            let result = fhir_client
413                .vread((), resource_type, id.clone(), version_id.clone())
414                .await?;
415
416            println!(
417                "{}",
418                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
419            );
420
421            Ok(())
422        }
423        ApiCommands::Batch { data, file, output } => {
424            let bundle = derive_resource_data_arg_file_arg_or_stdin::<Bundle>(data, file).await?;
425
426            let result = fhir_client.batch((), bundle).await?;
427
428            if let Some(true) = output {
429                println!(
430                    "{}",
431                    serde_json::to_string_pretty(&result).expect("Failed to serialize response")
432                );
433            }
434
435            Ok(())
436        }
437        ApiCommands::HistorySystem { parameters } => {
438            let result = fhir_client
439                .history_system(
440                    (),
441                    ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
442                )
443                .await?;
444
445            println!(
446                "{}",
447                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
448            );
449
450            Ok(())
451        }
452        ApiCommands::HistoryType {
453            resource_type,
454            parameters,
455        } => {
456            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
457                OperationOutcomeError::error(
458                    IssueType::invalid(),
459                    format!(
460                        "'{}' is not a valid FHIR resource type: {}",
461                        resource_type, e
462                    ),
463                )
464            })?;
465
466            let result = fhir_client
467                .history_type(
468                    (),
469                    resource_type,
470                    ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
471                )
472                .await?;
473
474            println!(
475                "{}",
476                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
477            );
478
479            Ok(())
480        }
481        ApiCommands::HistoryInstance {
482            resource_type,
483            id,
484            parameters,
485        } => {
486            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
487                OperationOutcomeError::error(
488                    IssueType::invalid(),
489                    format!(
490                        "'{}' is not a valid FHIR resource type: {}",
491                        resource_type, e
492                    ),
493                )
494            })?;
495
496            let result = fhir_client
497                .history_instance(
498                    (),
499                    resource_type,
500                    id.clone(),
501                    ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
502                )
503                .await?;
504
505            println!(
506                "{}",
507                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
508            );
509
510            Ok(())
511        }
512        ApiCommands::SearchType {
513            resource_type,
514            parameters,
515        } => {
516            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
517                OperationOutcomeError::error(
518                    IssueType::invalid(),
519                    format!(
520                        "'{}' is not a valid FHIR resource type: {}",
521                        resource_type, e
522                    ),
523                )
524            })?;
525
526            let result = fhir_client
527                .search_type(
528                    (),
529                    resource_type,
530                    ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
531                )
532                .await?;
533
534            println!(
535                "{}",
536                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
537            );
538
539            Ok(())
540        }
541        ApiCommands::SearchSystem { parameters } => {
542            let result = fhir_client
543                .search_system(
544                    (),
545                    ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
546                )
547                .await?;
548
549            println!(
550                "{}",
551                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
552            );
553
554            Ok(())
555        }
556        ApiCommands::InvokeSystem {
557            operation_name,
558            file,
559            data,
560        } => {
561            let parameters = derive_resource_data_arg_file_arg_or_stdin::<
562                haste_fhir_model::r4::generated::resources::Parameters,
563            >(data, file)
564            .await?;
565
566            let result = fhir_client
567                .invoke_system((), operation_name.clone(), parameters)
568                .await?;
569
570            println!(
571                "{}",
572                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
573            );
574
575            Ok(())
576        }
577        ApiCommands::InvokeType {
578            resource_type,
579            operation_name,
580            file,
581            data,
582        } => {
583            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
584                OperationOutcomeError::error(
585                    IssueType::invalid(),
586                    format!(
587                        "'{}' is not a valid FHIR resource type: {}",
588                        resource_type, e
589                    ),
590                )
591            })?;
592
593            let parameters = derive_resource_data_arg_file_arg_or_stdin::<
594                haste_fhir_model::r4::generated::resources::Parameters,
595            >(data, file)
596            .await?;
597
598            let result = fhir_client
599                .invoke_type((), resource_type, operation_name.clone(), parameters)
600                .await?;
601
602            println!(
603                "{}",
604                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
605            );
606
607            Ok(())
608        }
609        ApiCommands::InvokeInstance {
610            resource_type,
611            id,
612            operation_name,
613            file,
614            data,
615        } => {
616            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
617                OperationOutcomeError::error(
618                    IssueType::invalid(),
619                    format!(
620                        "'{}' is not a valid FHIR resource type: {}",
621                        resource_type, e
622                    ),
623                )
624            })?;
625
626            let parameters = derive_resource_data_arg_file_arg_or_stdin::<
627                haste_fhir_model::r4::generated::resources::Parameters,
628            >(data, file)
629            .await?;
630
631            let result = fhir_client
632                .invoke_instance(
633                    (),
634                    resource_type,
635                    id.clone(),
636                    operation_name.clone(),
637                    parameters,
638                )
639                .await?;
640
641            println!(
642                "{}",
643                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
644            );
645
646            Ok(())
647        }
648        ApiCommands::Capabilities {} => {
649            let result = fhir_client.capabilities(()).await?;
650
651            println!(
652                "{}",
653                serde_json::to_string_pretty(&result).expect("Failed to serialize response")
654            );
655
656            Ok(())
657        }
658        ApiCommands::DeleteInstance { resource_type, id } => {
659            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
660                OperationOutcomeError::error(
661                    IssueType::invalid(),
662                    format!(
663                        "'{}' is not a valid FHIR resource type: {}",
664                        resource_type, e
665                    ),
666                )
667            })?;
668
669            fhir_client
670                .delete_instance((), resource_type.clone(), id.clone())
671                .await?;
672
673            println!(
674                "Resource of type '{}' with ID '{}' deleted.",
675                resource_type.as_ref(),
676                id
677            );
678
679            Ok(())
680        }
681        ApiCommands::DeleteType {
682            resource_type,
683            parameters,
684        } => {
685            let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
686                OperationOutcomeError::error(
687                    IssueType::invalid(),
688                    format!(
689                        "'{}' is not a valid FHIR resource type: {}",
690                        resource_type, e
691                    ),
692                )
693            })?;
694
695            let parsed_parameters =
696                ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?;
697
698            fhir_client
699                .delete_type((), resource_type.clone(), parsed_parameters)
700                .await?;
701
702            println!(
703                "Resources of type '{}' deleted based on provided parameters.",
704                resource_type.as_ref()
705            );
706
707            Ok(())
708        }
709        ApiCommands::DeleteSystem { parameters } => {
710            let parsed_parameters =
711                ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?;
712
713            fhir_client.delete_system((), parsed_parameters).await?;
714
715            println!("Resources deleted based on provided system-level parameters.");
716
717            Ok(())
718        }
719    }
720}