haste_health/commands/
api.rs

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