seaplane_cli/ops/
metadata.rs

1use std::io::Write;
2
3use seaplane::api::metadata::v1::KeyValue as KeyValueModel;
4use serde::Serialize;
5use tabwriter::TabWriter;
6use unicode_segmentation::UnicodeSegmentation;
7
8use crate::{
9    context::Ctx,
10    error::{CliError, Result},
11    ops::EncodedString,
12    printer::{printer, Output},
13};
14
15/// We use our own KeyValue instead of the models because we need to *not* enforce base64 encoding,
16/// and implement a bunch of additional methods and traits that wouldn't make sense for the models
17///
18/// We also need to keep track if the values are encoded or not
19#[derive(Debug, Default, Clone, Serialize)]
20pub struct KeyValue {
21    pub key: EncodedString,
22    pub value: EncodedString,
23}
24
25impl KeyValue {
26    /// Creates a new KeyValue from an encoded key and value. You must pinky promise the key and
27    /// value are URL safe base64 encoded or Bad Things may happen.
28    pub fn new<S: Into<String>>(key: S, value: S) -> Self {
29        Self { key: EncodedString::new(key.into()), value: EncodedString::new(value.into()) }
30    }
31
32    /// Creates a new KeyValue from an un-encoded key and value, encoding them along the way
33    pub fn new_unencoded<S: AsRef<str>>(key: S, value: S) -> Self {
34        let engine = ::base64::engine::fast_portable::FastPortable::from(
35            &::base64::alphabet::URL_SAFE,
36            ::base64::engine::fast_portable::NO_PAD,
37        );
38        Self::new(
39            base64::encode_engine(key.as_ref(), &engine),
40            base64::encode_engine(value.as_ref(), &engine),
41        )
42    }
43
44    /// Creates a new KeyValue from an un-encoded string ref, encoding it along the way
45    pub fn from_key_unencoded<S: AsRef<str>>(key: S) -> Self {
46        let engine = ::base64::engine::fast_portable::FastPortable::from(
47            &::base64::alphabet::URL_SAFE,
48            ::base64::engine::fast_portable::NO_PAD,
49        );
50        Self::from_key(base64::encode_engine(key.as_ref(), &engine))
51    }
52
53    /// Creates a new KeyValue from an already encoded string ref. You must pinky promise the key
54    /// is URL safe base64 encoded or Bad Things may happen.
55    pub fn from_key<S: Into<String>>(key: S) -> Self {
56        Self { key: EncodedString::new(key.into()), ..Self::default() }
57    }
58
59    /// Sets the value to some base64 encoded value
60    pub fn set_value<S: Into<String>>(&mut self, value: S) {
61        self.value = EncodedString::new(value.into())
62    }
63}
64
65#[derive(Debug, Default, Clone, Serialize)]
66#[serde(transparent)]
67pub struct KeyValues {
68    inner: Vec<KeyValue>,
69}
70
71impl KeyValues {
72    pub fn from_model(model: Vec<KeyValueModel>) -> Self {
73        Self {
74            inner: model
75                .iter()
76                .map(|kv| KeyValue::new(kv.key.as_ref(), kv.value.as_ref()))
77                .collect(),
78        }
79    }
80
81    /// Inserts an already base64 encoded key and value
82    pub fn insert<S: Into<String>>(&mut self, key: S, value: S) {
83        self.inner.push(KeyValue::new(key, value));
84    }
85
86    pub fn iter(&self) -> impl Iterator<Item = &KeyValue> { self.inner.iter() }
87
88    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut KeyValue> { self.inner.iter_mut() }
89
90    pub fn push(&mut self, kv: KeyValue) { self.inner.push(kv) }
91
92    pub fn keys(&self) -> impl Iterator<Item = EncodedString> + '_ {
93        self.inner.iter().map(|kv| kv.key.clone())
94    }
95}
96
97impl Output for KeyValues {
98    fn print_json(&self, _ctx: &Ctx) -> Result<()> {
99        cli_println!("{}", serde_json::to_string(self)?);
100        Ok(())
101    }
102
103    fn print_table(&self, ctx: &Ctx) -> Result<()> {
104        let mdctx = ctx.md_ctx.get_or_init();
105        let headers = !mdctx.no_header;
106        let decode = mdctx.decode;
107        let decode_safe = mdctx.decode_safe;
108        let only_keys = mdctx.no_values;
109        let only_values = mdctx.no_keys;
110        let keys_width_limit = mdctx.keys_width_limit;
111        let values_width_limit = mdctx.values_width_limit;
112
113        let mut tw = TabWriter::new(Vec::new());
114
115        if headers {
116            match [only_keys, only_values] {
117                [true, false] => writeln!(tw, "KEY")?,
118                [false, true] => writeln!(tw, "VALUE")?,
119                [..] => writeln!(tw, "KEY\tVALUE")?,
120            }
121        }
122
123        // Truncate a binary vector to a specific length in bytes.
124        // Appends ellipsis `…` if the vector was truncated
125        fn truncate_vec(vec: Vec<u8>, max: usize) -> Vec<u8> {
126            if max == 0 || vec.len() <= max {
127                vec
128            } else {
129                let mut vec = vec;
130                vec.truncate(max);
131                vec.extend_from_slice("…".as_bytes());
132                vec
133            }
134        }
135
136        // Truncate a unicode string to a specific number of grapheme clusters.
137        // Appends ellipsis `…` if the string was truncated
138        fn truncate_string(str: String, max: usize) -> String {
139            if max == 0 || str.graphemes(true).count() <= max {
140                str
141            } else {
142                let mut s: String = str.graphemes(true).take(max).collect();
143                s.push('…');
144                s
145            }
146        }
147
148        for kv in self.iter() {
149            if !only_values {
150                if decode {
151                    tw.write_all(&truncate_vec(kv.key.decoded()?, keys_width_limit))?;
152                } else if decode_safe {
153                    write!(tw, "{}", truncate_string(kv.key.decoded_safe()?, keys_width_limit))?;
154                } else {
155                    write!(tw, "{}", truncate_string(kv.key.to_string(), keys_width_limit))?;
156                }
157
158                if !only_keys {
159                    tw.write_all(b"\t")?
160                };
161            };
162
163            if !only_keys {
164                if decode {
165                    tw.write_all(&truncate_vec(kv.value.decoded()?, values_width_limit))?;
166                } else if decode_safe {
167                    write!(
168                        tw,
169                        "{}",
170                        truncate_string(kv.value.decoded_safe()?, values_width_limit)
171                    )?;
172                } else {
173                    write!(tw, "{}", truncate_string(kv.value.to_string(), values_width_limit))?;
174                }
175            };
176            writeln!(tw)?;
177        }
178        tw.flush()?;
179
180        let mut ptr = printer();
181        let page = tw
182            .into_inner()
183            .map_err(|_| CliError::bail("IO flush error writing key-values"))?;
184        ptr.write_all(&page)?;
185        ptr.flush()?;
186
187        Ok(())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use serde_json::json;
194
195    use super::*;
196
197    fn build_kvs() -> KeyValues {
198        KeyValues {
199            inner: vec![
200                KeyValue {
201                    key: EncodedString::new("a2V5MQ".into()),
202                    value: EncodedString::new("dmFsdWUx".into()),
203                },
204                KeyValue {
205                    key: EncodedString::new("a2V5Mg".into()),
206                    value: EncodedString::new("dmFsdWUy".into()),
207                },
208                KeyValue {
209                    key: EncodedString::new("a2V5Mw".into()),
210                    value: EncodedString::new("dmFsdWUz".into()),
211                },
212            ],
213        }
214    }
215
216    #[test]
217    fn serialize_keyvalues_base64() {
218        let kvs = build_kvs();
219
220        assert_eq!(
221            serde_json::to_string(&kvs).unwrap(),
222            json!([{"key": "a2V5MQ", "value": "dmFsdWUx"}, {"key": "a2V5Mg", "value": "dmFsdWUy"}, {"key": "a2V5Mw", "value": "dmFsdWUz"}]).to_string()
223        );
224    }
225}