reqlang_expr/cliutil.rs
1//! A set of utility functions to implement reglang-expr CLIs
2
3use std::{
4 error::Error,
5 fs::read_to_string,
6 io::{Read, stdin},
7};
8
9/// Unzip a vector of key-value pairs into separate vectors for keys and values.
10///
11/// ```
12/// use reqlang_expr::cliutil::unzip_key_values;
13///
14/// let keys_values: Vec<(String, String)> = vec![
15/// ("key1".to_string(), "val1".to_string()),
16/// ("key2".to_string(), "val2".to_string())
17/// ];
18///
19/// let (keys, values) = unzip_key_values(keys_values);
20///
21/// assert_eq!(vec!["key1", "key2"], keys);
22/// assert_eq!(vec!["val1", "val2"], values);
23/// ```
24///
25/// ## Usage
26///
27/// This is can be used to accept in key-value pairs from command line arguments,
28/// feeding the keys/values to the compile time and runtime envrionments
29/// respectively.
30///
31/// ```ignore
32/// let (var_keys, var_values) = unzip_key_values(args.vars);
33/// let (prompt_keys, prompt_values) = unzip_key_values(args.prompts);
34/// let (secret_keys, secret_values) = unzip_key_values(args.secrets);
35///
36/// let env = Env::new(var_keys, prompt_keys, secret_keys);
37///
38/// let runtime_env: RuntimeEnv = RuntimeEnv {
39/// vars: var_values,
40/// prompts: prompt_values,
41/// secrets: secret_values,
42/// };
43/// ```
44pub fn unzip_key_values(keys_values: Vec<(String, String)>) -> (Vec<String>, Vec<String>) {
45 let (keys, values): (Vec<String>, Vec<String>) = keys_values.into_iter().unzip();
46
47 (keys, values)
48}
49
50/// Parse a single key-value pair string. This is used to parse command line arguments.
51///
52/// ```
53/// use reqlang_expr::cliutil::parse_key_val;
54///
55/// let a = parse_key_val::<String, String>("a=1");
56///
57/// assert_eq!(("a".to_string(), "1".to_string()), a.unwrap());
58/// ```
59///
60/// Example of using parse_key_val with Clap
61///
62/// ```
63/// use clap::Parser;
64/// use reqlang_expr::cliutil::parse_key_val;
65///
66/// #[derive(Parser, Debug)]
67/// struct Args {
68/// #[arg(long, value_delimiter = ' ', num_args = 1.., value_parser = parse_key_val::<String, String>)]
69/// vars: Vec<(String, String)>
70/// }
71///
72/// let args = Args::parse_from(["test", "--vars", "key=value", "another_key=another_value"]);
73/// assert_eq!(args.vars, vec![("key".to_string(), "value".to_string()), ("another_key".to_string(), "another_value".to_string())]);
74/// ```
75pub fn parse_key_val<T, U>(value: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
76where
77 T: std::str::FromStr,
78 T::Err: Error + Send + Sync + 'static,
79 U: std::str::FromStr,
80 U::Err: Error + Send + Sync + 'static,
81{
82 let n = 2;
83
84 let parts: Vec<&str> = value.splitn(n, '=').collect();
85
86 if parts.len() != n {
87 return Err(format!("should be formatted as key=value pair: `{value}`").into());
88 }
89
90 let key = parts[0].parse()?;
91 let value = parts[1].parse()?;
92
93 Ok((key, value))
94}
95
96/// Read source code from a file at the provided path or from standard input if no path is provided.
97///
98/// # Usage
99///
100/// ## From a file:
101///
102/// ```
103/// use reqlang_expr::cliutil::read_in_source;
104///
105/// let source_code = read_in_source(Some("./spec/valid/call_id.expr".to_string()));
106///
107/// assert_eq!("(id (noop))", source_code);
108/// ```
109///
110/// ## From stdin:
111///
112/// ```ignore
113/// use reqlang_expr::cliutil::read_in_source;
114///
115/// // Assuming "(id (noop))" was passed to stdin...
116///
117/// let source_code = read_in_source(None);
118///
119/// assert_eq!("(id (noop))".to_string(), source_code);
120/// ```
121pub fn read_in_source(path: Option<String>) -> String {
122 match path {
123 Some(path) => {
124 //
125 read_to_string(path).expect("should be able to open file at path")
126 }
127 None => {
128 let mut source = String::new();
129
130 stdin().read_to_string(&mut source).unwrap();
131
132 source
133 }
134 }
135}
136
137#[cfg(test)]
138mod cliutil_tests {
139 use clap::Parser;
140
141 use crate::cliutil::{parse_key_val, read_in_source};
142
143 #[test]
144 fn read_in_source_from_file() {
145 let result = read_in_source(Some("./spec/valid/call_id.expr".to_string()));
146
147 assert_eq!("(id (noop))", result);
148 }
149
150 #[test]
151 fn parse_key_val_valid_keyvalue_pair() {
152 #[derive(Parser, Debug, PartialEq)]
153 struct Args {
154 #[arg(long, value_delimiter = ' ', num_args = 1.., value_parser = parse_key_val::<String, String>)]
155 vars: Vec<(String, String)>,
156 }
157
158 assert_eq!(
159 Args {
160 vars: vec![(String::from("key"), String::from("value"))]
161 },
162 Args::try_parse_from(["test", "--vars", "key=value"])
163 .ok()
164 .unwrap()
165 );
166 }
167
168 #[test]
169 fn parse_key_val_invalid_keyvalue_pair() {
170 #[derive(Parser, Debug, PartialEq)]
171 struct Args {
172 #[arg(long, value_delimiter = ' ', num_args = 1.., value_parser = parse_key_val::<String, String>)]
173 vars: Vec<(String, String)>,
174 }
175
176 assert_eq!(
177 "error: invalid value 'key_without_value' for '--vars <VARS>...': should be formatted as key=value pair: `key_without_value`\n\nFor more information, try '--help'.\n",
178 Args::try_parse_from(["test", "--vars", "key_without_value"])
179 .err()
180 .unwrap()
181 .to_string()
182 );
183 }
184}