haste_health/commands/
package.rs1use crate::cli::state::CliState;
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use clap::Subcommand;
7use haste_fhir_client::{FHIRClient, url::ParsedParameters};
8use haste_fhir_generated_ops::generated::{
9 HasteHealthImplementationguideInstall as Install,
10 HasteHealthImplementationguideUninstall as Uninstall,
11};
12use haste_fhir_model::r4::generated::{
13 resources::{Parameters, Resource, ResourceType},
14 terminology::IssueType,
15 types::{FHIRBase64Binary, FHIRBoolean, FHIRString},
16};
17use haste_fhir_operation_error::OperationOutcomeError;
18use std::{path::Path, sync::Arc};
19use tokio::sync::Mutex;
20
21#[derive(Subcommand, Debug)]
25pub(crate) enum PackageCommands {
26 Install {
28 #[arg(required = true)]
33 packages: Vec<String>,
34 #[arg(long)]
36 no_dependencies: bool,
37 #[arg(long)]
39 examples: bool,
40 #[arg(long)]
43 json: bool,
44 },
45 Uninstall {
48 name: String,
50 #[arg(long)]
52 version: Option<String>,
53 },
54 List,
56}
57
58fn fhir_string(value: impl Into<String>) -> FHIRString {
59 FHIRString {
60 value: Some(value.into()),
61 ..Default::default()
62 }
63}
64
65fn value(string: Option<&FHIRString>) -> &str {
66 string.and_then(|s| s.value.as_deref()).unwrap_or_default()
67}
68
69fn upload(argument: &str) -> Result<Option<FHIRBase64Binary>, OperationOutcomeError> {
71 let path = Path::new(argument);
72 let bytes = if path.is_dir() {
73 haste_fhir_package::pack(path)?
74 } else if path.is_file() {
75 std::fs::read(path).map_err(|e| {
76 OperationOutcomeError::error(
77 IssueType::exception(),
78 format!("Failed to read '{argument}': {e}"),
79 )
80 })?
81 } else {
82 return Ok(None);
83 };
84 Ok(Some(FHIRBase64Binary {
85 value: Some(STANDARD.encode(bytes)),
86 ..Default::default()
87 }))
88}
89
90fn parameters(
91 input: impl Into<Vec<haste_fhir_model::r4::generated::resources::ParametersParameter>>,
92) -> Parameters {
93 Parameters {
94 parameter: Some(input.into()),
95 ..Default::default()
96 }
97}
98
99fn print_install_summary(output: &Install::Output) {
100 for package in output.package.iter().flatten() {
101 let name = value(Some(&package.name));
102 let version = value(Some(&package.version));
103 let status = package.status.value.as_deref().unwrap_or_default();
104 let resources = package
105 .resources
106 .as_ref()
107 .and_then(|count| count.value)
108 .unwrap_or_default();
109 match status {
110 "skipped" => println!(
111 "skipped {name}@{version}: {}",
112 value(package.reason.as_ref())
113 ),
114 "updated" => println!(
115 "updated {name}@{version} (was {}): {resources} resources",
116 value(package.previousVersion.as_ref())
117 ),
118 _ => println!("{status:<10} {name}@{version}: {resources} resources"),
119 }
120 }
121 let warnings = output
122 .outcome
123 .iter()
124 .flat_map(|outcome| &outcome.issue)
125 .filter_map(|issue| issue.diagnostics.as_ref()?.value.as_deref())
126 .collect::<Vec<_>>();
127 if !warnings.is_empty() {
128 println!("\n{} warning(s):", warnings.len());
129 for warning in warnings {
130 println!(" {warning}");
131 }
132 }
133}
134
135pub(crate) async fn run(
137 state: Arc<Mutex<CliState>>,
138 command: &PackageCommands,
139) -> Result<(), OperationOutcomeError> {
140 let fhir_client = crate::cli::client::fhir_client(state).await?;
141
142 match command {
143 PackageCommands::Install {
144 packages,
145 no_dependencies,
146 examples,
147 json,
148 } => {
149 let mut named = Vec::new();
150 let mut uploads = Vec::new();
151 for argument in packages {
152 match upload(argument)? {
153 Some(content) => uploads.push(content),
154 None => named.push(fhir_string(argument)),
155 }
156 }
157 let input = Install::Input {
158 package: Some(named).filter(|named| !named.is_empty()),
159 npmContent: Some(uploads).filter(|uploads| !uploads.is_empty()),
160 dependencies: Some(FHIRBoolean {
161 value: Some(!no_dependencies),
162 ..Default::default()
163 }),
164 examples: Some(FHIRBoolean {
165 value: Some(*examples),
166 ..Default::default()
167 }),
168 };
169
170 let result = fhir_client
171 .invoke_type(
172 (),
173 ResourceType::ImplementationGuide,
174 "install".to_string(),
175 parameters(input),
176 )
177 .await?;
178
179 if *json {
180 println!(
181 "{}",
182 serde_json::to_string_pretty(&result).expect("Failed to serialize response")
183 );
184 return Ok(());
185 }
186 let Resource::Parameters(result) = result else {
187 return Err(OperationOutcomeError::error(
188 IssueType::exception(),
189 "The server answered $install with something other than Parameters."
190 .to_string(),
191 ));
192 };
193 print_install_summary(&Install::Output::try_from(
194 result.parameter.unwrap_or_default(),
195 )?);
196 Ok(())
197 }
198 PackageCommands::Uninstall { name, version } => {
199 let input = Uninstall::Input {
200 name: fhir_string(name),
201 version: version.as_ref().map(fhir_string),
202 };
203 let result = fhir_client
204 .invoke_type(
205 (),
206 ResourceType::ImplementationGuide,
207 "uninstall".to_string(),
208 parameters(input),
209 )
210 .await?;
211 match result {
212 Resource::OperationOutcome(outcome) => {
213 for issue in outcome.issue {
214 println!("{}", value(issue.diagnostics.as_deref()));
215 }
216 }
217 other => println!(
218 "{}",
219 serde_json::to_string_pretty(&other).expect("Failed to serialize response")
220 ),
221 }
222 Ok(())
223 }
224 PackageCommands::List => {
225 let bundle = fhir_client
226 .search_type(
227 (),
228 ResourceType::ImplementationGuide,
229 ParsedParameters::try_from("_count=1000")?,
230 )
231 .await?;
232 for entry in bundle.entry.unwrap_or_default() {
233 if let Some(Resource::ImplementationGuide(guide)) = entry.resource.as_deref() {
234 let title = guide
235 .title
236 .as_deref()
237 .and_then(|title| title.value.as_deref())
238 .unwrap_or_default();
239 println!(
240 "{}@{} {title}",
241 guide.packageId.value.as_deref().unwrap_or_default(),
242 value(guide.version.as_deref()),
243 );
244 }
245 }
246 Ok(())
247 }
248 }
249}