Skip to main content

openstack_cli_core/
common.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! Common helpers.
16use 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/// Newtype for the `HashMap<String, String>`
36#[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
55// /// Try to deserialize data and return `Default` if that fails
56// pub fn deser_ok_or_default<'a, T, D>(deserializer: D) -> Result<T, D::Error>
57// where
58//     T: Deserialize<'a> + Default,
59//     D: Deserializer<'a>,
60// {
61//     let v: Value = Deserialize::deserialize(deserializer)?;
62//     Ok(T::deserialize(v).unwrap_or_default())
63// }
64
65/// Parse a single key-value pair
66pub 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
79/// Parse a single key-value pair where value can be null
80pub 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
105/// Download content from the reqwests response stream.
106/// When dst_name = "-" - write content to the stdout.
107/// Otherwise write into the destination and display progress_bar
108pub 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
149/// Construct BoxedAsyncRead with progress bar from stdin
150pub 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
165/// Construct BoxedAsyncRead with progress bar from the file
166pub 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
185/// Wrap file or stdout for being uploaded with reqwests library.
186/// When dst_name = "-" - write content to the stdout.
187/// Otherwise write into the destination and display progress_bar
188pub 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        // Reading from stdin
193        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// #[derive(Debug, PartialEq, PartialOrd)]
208// pub(crate) struct ServiceApiVersion(pub u8, pub u8);
209//
210// impl TryFrom<String> for ServiceApiVersion {
211//     type Error = ();
212//     fn try_from(ver: String) -> Result<Self, Self::Error> {
213//         let parts: Vec<u8> = ver.split('.').flat_map(|v| v.parse::<u8>()).collect();
214//         Ok(ServiceApiVersion(parts[0], parts[1]))
215//     }
216// }
217
218#[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}