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 ¤t_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 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(¶ms)
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 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}