1use crate::{cli::state::CliState, utilities};
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#[derive(Subcommand, Debug)]
17pub(crate) enum ApiCommands {
18 Create {
20 #[arg(short, long)]
22 data: Option<String>,
23 #[arg(short, long)]
25 file: Option<String>,
26 resource_type: String,
28 },
29 Read { resource_type: String, id: String },
31
32 VersionRead {
34 resource_type: String,
35 id: String,
36 version_id: String,
37 },
38
39 Patch {
41 #[arg(short, long)]
43 data: Option<String>,
44 #[arg(short, long)]
46 file: Option<String>,
47 resource_type: String,
48 id: String,
49 },
50 Update {
52 #[arg(short, long)]
54 data: Option<String>,
55 #[arg(short, long)]
57 file: Option<String>,
58 resource_type: String,
59 id: String,
60 },
61 Transaction {
63 #[arg(short, long)]
65 data: Option<String>,
66 #[arg(short, long)]
68 parallel: Option<usize>,
69 #[arg(short, long)]
71 file: Option<String>,
72 #[arg(short, long)]
74 output: Option<bool>,
75 },
76 Batch {
78 #[arg(short, long)]
80 data: Option<String>,
81 #[arg(short, long)]
83 file: Option<String>,
84 #[arg(short, long)]
86 output: Option<bool>,
87 },
88
89 HistorySystem {
91 parameters: Option<String>,
93 },
94
95 HistoryType {
97 resource_type: String,
98 parameters: Option<String>,
100 },
101
102 HistoryInstance {
104 resource_type: String,
105 id: String,
106 parameters: Option<String>,
108 },
109
110 SearchType {
112 resource_type: String,
113 parameters: Option<String>,
115 },
116
117 SearchSystem {
119 parameters: Option<String>,
121 },
122
123 InvokeSystem {
125 #[arg(short, long)]
127 data: Option<String>,
128 #[arg(short, long)]
130 file: Option<String>,
131 operation_name: String,
132 },
133
134 InvokeType {
136 #[arg(short, long)]
138 data: Option<String>,
139 #[arg(short, long)]
141 file: Option<String>,
142 resource_type: String,
143 operation_name: String,
144 },
145
146 Capabilities {},
148
149 DeleteInstance { resource_type: String, id: String },
151
152 DeleteType {
154 resource_type: String,
155 parameters: Option<String>,
157 },
158
159 DeleteSystem {
161 parameters: Option<String>,
163 },
164
165 InvokeInstance {
167 #[arg(short, long)]
169 data: Option<String>,
170 #[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 let mut buffer = String::new();
207 utilities::read_stdin(&mut buffer)?;
208
209 serde_json::from_str::<Type>(&buffer).map_err(|e| {
210 OperationOutcomeError::error(
211 IssueType::exception(),
212 format!("Failed to parse transaction from stdin: {}", e),
213 )
214 })
215 }
216}
217
218pub(crate) async fn run(
220 state: Arc<Mutex<CliState>>,
221 command: &ApiCommands,
222) -> Result<(), OperationOutcomeError> {
223 let fhir_client = crate::cli::client::fhir_client(state).await?;
224
225 match command {
226 ApiCommands::Create {
227 data,
228 resource_type,
229 file,
230 } => {
231 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
232 OperationOutcomeError::error(
233 IssueType::invalid(),
234 format!(
235 "'{}' is not a valid FHIR resource type: {}",
236 resource_type, e
237 ),
238 )
239 })?;
240
241 let resource =
242 derive_resource_data_arg_file_arg_or_stdin::<Resource>(data, file).await?;
243
244 let result = fhir_client.create((), resource_type, resource).await?;
245
246 println!(
247 "{}",
248 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
249 );
250
251 Ok(())
252 }
253 ApiCommands::Read { resource_type, id } => {
254 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
255 OperationOutcomeError::error(
256 IssueType::invalid(),
257 format!(
258 "'{}' is not a valid FHIR resource type: {}",
259 resource_type, e
260 ),
261 )
262 })?;
263
264 let result = fhir_client.read((), resource_type, id.clone()).await?;
265
266 println!(
267 "{}",
268 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
269 );
270
271 Ok(())
272 }
273 ApiCommands::Patch {
274 resource_type,
275 id,
276 data,
277 file,
278 } => {
279 let patches = if let Some(file) = file {
280 let file_content = tokio::fs::read_to_string(file).await.map_err(|e| {
281 OperationOutcomeError::error(
282 IssueType::exception(),
283 format!("Failed to read transaction file: {}", e),
284 )
285 })?;
286
287 serde_json::from_str::<json_patch::Patch>(&file_content).map_err(|e| {
288 OperationOutcomeError::error(
289 IssueType::invalid(),
290 format!("Failed to parse patch JSON: {}", e),
291 )
292 })?
293 } else if let Some(data) = data {
294 serde_json::from_str::<json_patch::Patch>(&data).map_err(|e| {
295 OperationOutcomeError::error(
296 IssueType::invalid(),
297 format!("Failed to parse patch JSON: {}", e),
298 )
299 })?
300 } else {
301 return Err(OperationOutcomeError::error(
302 IssueType::invalid(),
303 "Either --data or --file must be provided for patch operation.".to_string(),
304 ));
305 };
306
307 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
308 OperationOutcomeError::error(
309 IssueType::invalid(),
310 format!(
311 "'{}' is not a valid FHIR resource type: {}",
312 resource_type, e
313 ),
314 )
315 })?;
316
317 let result = fhir_client
318 .patch((), resource_type, id.clone(), patches)
319 .await?;
320
321 println!(
322 "{}",
323 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
324 );
325
326 Ok(())
327 }
328 ApiCommands::Update {
329 resource_type,
330 id,
331 data,
332 file,
333 } => {
334 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
335 OperationOutcomeError::error(
336 IssueType::invalid(),
337 format!(
338 "'{}' is not a valid FHIR resource type: {}",
339 resource_type, e
340 ),
341 )
342 })?;
343
344 let resource =
345 derive_resource_data_arg_file_arg_or_stdin::<Resource>(data, file).await?;
346
347 let result = fhir_client
348 .update((), resource_type, id.clone(), resource)
349 .await?;
350
351 println!(
352 "{}",
353 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
354 );
355 Ok(())
356 }
357 ApiCommands::Transaction {
358 data,
359 file,
360 output,
361 parallel,
362 } => {
363 let bundle = derive_resource_data_arg_file_arg_or_stdin::<Bundle>(data, file).await?;
364
365 let parallel = parallel.unwrap_or(1);
366
367 let mut futures = tokio::task::JoinSet::new();
368
369 for _ in 0..parallel {
370 let client = fhir_client.clone();
371 let bundle = bundle.clone();
372 let res = async move { client.transaction((), bundle).await };
373 futures.spawn(res);
374 }
375
376 let res = futures.join_all().await;
377
378 for bundle_result in res {
379 let bundle = bundle_result?;
380 if let Some(true) = output {
381 println!(
382 "{}",
383 serde_json::to_string_pretty(&bundle)
384 .expect("Failed to serialize response")
385 );
386 }
387 }
388
389 Ok(())
390 }
391 ApiCommands::VersionRead {
392 resource_type,
393 id,
394 version_id,
395 } => {
396 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
397 OperationOutcomeError::error(
398 IssueType::invalid(),
399 format!(
400 "'{}' is not a valid FHIR resource type: {}",
401 resource_type, e
402 ),
403 )
404 })?;
405
406 let result = fhir_client
407 .vread((), resource_type, id.clone(), version_id.clone())
408 .await?;
409
410 println!(
411 "{}",
412 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
413 );
414
415 Ok(())
416 }
417 ApiCommands::Batch { data, file, output } => {
418 let bundle = derive_resource_data_arg_file_arg_or_stdin::<Bundle>(data, file).await?;
419
420 let result = fhir_client.batch((), bundle).await?;
421
422 if let Some(true) = output {
423 println!(
424 "{}",
425 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
426 );
427 }
428
429 Ok(())
430 }
431 ApiCommands::HistorySystem { parameters } => {
432 let result = fhir_client
433 .history_system(
434 (),
435 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
436 )
437 .await?;
438
439 println!(
440 "{}",
441 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
442 );
443
444 Ok(())
445 }
446 ApiCommands::HistoryType {
447 resource_type,
448 parameters,
449 } => {
450 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
451 OperationOutcomeError::error(
452 IssueType::invalid(),
453 format!(
454 "'{}' is not a valid FHIR resource type: {}",
455 resource_type, e
456 ),
457 )
458 })?;
459
460 let result = fhir_client
461 .history_type(
462 (),
463 resource_type,
464 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
465 )
466 .await?;
467
468 println!(
469 "{}",
470 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
471 );
472
473 Ok(())
474 }
475 ApiCommands::HistoryInstance {
476 resource_type,
477 id,
478 parameters,
479 } => {
480 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
481 OperationOutcomeError::error(
482 IssueType::invalid(),
483 format!(
484 "'{}' is not a valid FHIR resource type: {}",
485 resource_type, e
486 ),
487 )
488 })?;
489
490 let result = fhir_client
491 .history_instance(
492 (),
493 resource_type,
494 id.clone(),
495 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
496 )
497 .await?;
498
499 println!(
500 "{}",
501 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
502 );
503
504 Ok(())
505 }
506 ApiCommands::SearchType {
507 resource_type,
508 parameters,
509 } => {
510 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
511 OperationOutcomeError::error(
512 IssueType::invalid(),
513 format!(
514 "'{}' is not a valid FHIR resource type: {}",
515 resource_type, e
516 ),
517 )
518 })?;
519
520 let result = fhir_client
521 .search_type(
522 (),
523 resource_type,
524 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
525 )
526 .await?;
527
528 println!(
529 "{}",
530 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
531 );
532
533 Ok(())
534 }
535 ApiCommands::SearchSystem { parameters } => {
536 let result = fhir_client
537 .search_system(
538 (),
539 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?,
540 )
541 .await?;
542
543 println!(
544 "{}",
545 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
546 );
547
548 Ok(())
549 }
550 ApiCommands::InvokeSystem {
551 operation_name,
552 file,
553 data,
554 } => {
555 let parameters = derive_resource_data_arg_file_arg_or_stdin::<
556 haste_fhir_model::r4::generated::resources::Parameters,
557 >(data, file)
558 .await?;
559
560 let result = fhir_client
561 .invoke_system((), operation_name.clone(), parameters)
562 .await?;
563
564 println!(
565 "{}",
566 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
567 );
568
569 Ok(())
570 }
571 ApiCommands::InvokeType {
572 resource_type,
573 operation_name,
574 file,
575 data,
576 } => {
577 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
578 OperationOutcomeError::error(
579 IssueType::invalid(),
580 format!(
581 "'{}' is not a valid FHIR resource type: {}",
582 resource_type, e
583 ),
584 )
585 })?;
586
587 let parameters = derive_resource_data_arg_file_arg_or_stdin::<
588 haste_fhir_model::r4::generated::resources::Parameters,
589 >(data, file)
590 .await?;
591
592 let result = fhir_client
593 .invoke_type((), resource_type, operation_name.clone(), parameters)
594 .await?;
595
596 println!(
597 "{}",
598 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
599 );
600
601 Ok(())
602 }
603 ApiCommands::InvokeInstance {
604 resource_type,
605 id,
606 operation_name,
607 file,
608 data,
609 } => {
610 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
611 OperationOutcomeError::error(
612 IssueType::invalid(),
613 format!(
614 "'{}' is not a valid FHIR resource type: {}",
615 resource_type, e
616 ),
617 )
618 })?;
619
620 let parameters = derive_resource_data_arg_file_arg_or_stdin::<
621 haste_fhir_model::r4::generated::resources::Parameters,
622 >(data, file)
623 .await?;
624
625 let result = fhir_client
626 .invoke_instance(
627 (),
628 resource_type,
629 id.clone(),
630 operation_name.clone(),
631 parameters,
632 )
633 .await?;
634
635 println!(
636 "{}",
637 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
638 );
639
640 Ok(())
641 }
642 ApiCommands::Capabilities {} => {
643 let result = fhir_client.capabilities(()).await?;
644
645 println!(
646 "{}",
647 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
648 );
649
650 Ok(())
651 }
652 ApiCommands::DeleteInstance { resource_type, id } => {
653 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
654 OperationOutcomeError::error(
655 IssueType::invalid(),
656 format!(
657 "'{}' is not a valid FHIR resource type: {}",
658 resource_type, e
659 ),
660 )
661 })?;
662
663 fhir_client
664 .delete_instance((), resource_type.clone(), id.clone())
665 .await?;
666
667 println!(
668 "Resource of type '{}' with ID '{}' deleted.",
669 resource_type.as_ref(),
670 id
671 );
672
673 Ok(())
674 }
675 ApiCommands::DeleteType {
676 resource_type,
677 parameters,
678 } => {
679 let resource_type = ResourceType::try_from(resource_type.as_str()).map_err(|e| {
680 OperationOutcomeError::error(
681 IssueType::invalid(),
682 format!(
683 "'{}' is not a valid FHIR resource type: {}",
684 resource_type, e
685 ),
686 )
687 })?;
688
689 let parsed_parameters =
690 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?;
691
692 fhir_client
693 .delete_type((), resource_type.clone(), parsed_parameters)
694 .await?;
695
696 println!(
697 "Resources of type '{}' deleted based on provided parameters.",
698 resource_type.as_ref()
699 );
700
701 Ok(())
702 }
703 ApiCommands::DeleteSystem { parameters } => {
704 let parsed_parameters =
705 ParsedParameters::try_from(parameters.clone().unwrap_or_default().as_str())?;
706
707 fhir_client.delete_system((), parsed_parameters).await?;
708
709 println!("Resources deleted based on provided system-level parameters.");
710
711 Ok(())
712 }
713 }
714}