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#[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
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
224pub(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}