openstack_cli_core/
common.rs1use eyre::OptionExt;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20use std::error::Error;
21use std::io::IsTerminal;
22
23use indicatif::{ProgressBar, ProgressStyle};
24use std::path::Path;
25use tokio::fs;
26use tokio::io::{self, copy};
27use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
28use tokio_util::io::InspectReader;
29
30use openstack_sdk_core::types::BoxedAsyncRead;
31use structable::{StructTable, StructTableOptions};
32
33use crate::error::OpenStackCliError;
34
35#[derive(Deserialize, Default, Debug, Clone, Serialize)]
37pub struct HashMapStringString(pub HashMap<String, String>);
38
39impl StructTable for HashMapStringString {
40 fn instance_headers<O: StructTableOptions>(
41 &self,
42 _options: &O,
43 ) -> Option<::std::vec::Vec<::std::string::String>> {
44 Some(self.0.keys().map(Into::into).collect())
45 }
46
47 fn data<O: StructTableOptions>(
48 &self,
49 _options: &O,
50 ) -> ::std::vec::Vec<Option<::std::string::String>> {
51 self.0.values().map(|x| Some(x.into())).collect()
52 }
53}
54
55pub fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
67where
68 T: std::str::FromStr,
69 T::Err: Error + Send + Sync + 'static,
70 U: std::str::FromStr,
71 U::Err: Error + Send + Sync + 'static,
72{
73 let (k, v) = s
74 .split_once('=')
75 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
76 Ok((k.parse()?, v.parse()?))
77}
78
79pub fn parse_key_val_opt<T, U>(
81 s: &str,
82) -> Result<(T, Option<U>), Box<dyn Error + Send + Sync + 'static>>
83where
84 T: std::str::FromStr,
85 T::Err: Error + Send + Sync + 'static,
86 U: std::str::FromStr,
87 U::Err: Error + Send + Sync + 'static,
88{
89 let (k, v) = s
90 .split_once('=')
91 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
92
93 let key = k.parse()?;
94 let val = (!v.is_empty()).then(|| v.parse()).transpose()?;
95
96 Ok((key, val))
97}
98
99pub fn parse_json(s: &str) -> Result<Value, Box<dyn Error + Send + Sync + 'static>>
100where
101{
102 Ok(serde_json::from_str(s)?)
103}
104
105pub async fn download_file(
109 dst_name: String,
110 size: u64,
111 data: BoxedAsyncRead,
112) -> Result<(), OpenStackCliError> {
113 let progress_bar = ProgressBar::new(size);
114
115 let mut inspect_reader =
116 InspectReader::new(data.compat(), |bytes| progress_bar.inc(bytes.len() as u64));
117 if dst_name == "-" {
118 progress_bar.set_style(
119 ProgressStyle::default_bar()
120 .progress_chars("#>-")
121 .template("[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec}")?,
122 );
123
124 let mut writer = io::stdout();
125 copy(&mut inspect_reader, &mut writer).await?;
126 } else {
127 let path = Path::new(&dst_name);
128 let fname = path
129 .file_name()
130 .ok_or_eyre("download file name must be known")?
131 .to_str()
132 .ok_or_eyre("download file name must be a string")?;
133 progress_bar.set_message(String::from(fname));
134 progress_bar.set_style(
135 ProgressStyle::default_bar()
136 .progress_chars("#>-")
137 .template(
138 "[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec} - {msg}",
139 )?,
140 );
141
142 let mut writer = fs::File::create(path).await?;
143 io::copy(&mut inspect_reader, &mut writer).await?;
144 }
145 progress_bar.finish();
146 Ok(())
147}
148
149pub async fn build_upload_asyncread_from_stdin() -> Result<BoxedAsyncRead, OpenStackCliError> {
151 let progress_bar = ProgressBar::new(0);
152
153 progress_bar.set_style(
154 ProgressStyle::default_bar()
155 .progress_chars("#>-")
156 .template("[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec}")?,
157 );
158
159 let inspect_reader = InspectReader::new(io::stdin(), move |bytes| {
160 progress_bar.inc(bytes.len() as u64)
161 });
162 Ok(BoxedAsyncRead::new(inspect_reader.compat()))
163}
164
165pub async fn build_upload_asyncread_from_file(
167 file_path: &str,
168) -> Result<BoxedAsyncRead, OpenStackCliError> {
169 let progress_bar = ProgressBar::new(0);
170
171 progress_bar.set_style(
172 ProgressStyle::default_bar()
173 .progress_chars("#>-")
174 .template("[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec}")?,
175 );
176 let reader = fs::File::open(&file_path).await?;
177
178 progress_bar.set_length(reader.metadata().await?.len());
179 let inspect_reader =
180 InspectReader::new(reader, move |bytes| progress_bar.inc(bytes.len() as u64));
181
182 Ok(BoxedAsyncRead::new(inspect_reader.compat()))
183}
184
185pub async fn build_upload_asyncread(
189 src_name: Option<String>,
190) -> Result<BoxedAsyncRead, OpenStackCliError> {
191 if !std::io::stdin().is_terminal() && src_name.is_none() {
192 build_upload_asyncread_from_stdin().await
194 } else {
195 match src_name
196 .ok_or(OpenStackCliError::InputParameters(
197 "upload source name must be provided when stdin is not being piped".into(),
198 ))?
199 .as_str()
200 {
201 "-" => build_upload_asyncread_from_stdin().await,
202 file_name => build_upload_asyncread_from_file(file_name).await,
203 }
204 }
205}
206
207#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_parse_key_val() {
224 assert_eq!(
225 ("foo".to_string(), "bar".to_string()),
226 parse_key_val::<String, String>("foo=bar").unwrap()
227 );
228 }
229
230 #[test]
231 fn test_parse_key_val_opt() {
232 assert_eq!(
233 ("foo".to_string(), Some("bar".to_string())),
234 parse_key_val_opt::<String, String>("foo=bar").unwrap()
235 );
236 assert_eq!(
237 ("foo".to_string(), None),
238 parse_key_val_opt::<String, String>("foo=").unwrap()
239 );
240 }
241}