snix_glue/builder/structured_attrs/
shell.rs1use regex::Regex;
5use std::sync::LazyLock;
6
7static RE_SH_VAR_NAME: LazyLock<Regex> =
8 LazyLock::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
9
10fn write_shell_escaped_single_quoted<W>(f: &mut W, s: &str) -> std::fmt::Result
12where
13 W: std::fmt::Write,
14{
15 write!(f, "'")?;
16 for c in s.chars() {
17 if c == '\'' {
18 write!(f, "'\\''")?;
19 } else {
20 write!(f, "{c}")?;
21 }
22 }
23 write!(f, "'")?;
24 Ok(())
25}
26
27fn is_good_simple_value(v: &serde_json::Value) -> bool {
30 match v {
31 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => true,
32 serde_json::Value::Number(number) => {
33 if number.as_i64().is_some() || number.as_u64().is_some() {
34 true
35 } else if let Some(n) = number.as_f64() {
36 n.ceil() == n
37 } else if number.as_i128().is_some() {
38 true
39 } else {
40 number.as_u128().is_some()
41 }
42 }
43 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
44 unreachable!("Snix bug: called write_simple_type on complex type")
45 }
46 }
47}
48
49fn write_simple_type<W>(f: &mut W, v: serde_json::Value) -> std::fmt::Result
50where
51 W: std::fmt::Write,
52{
53 match v {
54 serde_json::Value::Null => write!(f, "''")?,
55 serde_json::Value::Bool(v) => {
56 if v {
57 write!(f, "1")?;
58 } else {
59 write!(f, "")?;
60 }
61 }
62 serde_json::Value::Number(number) => {
63 if let Some(n) = number.as_i64() {
64 write!(f, "{n}")?;
65 } else if let Some(n) = number.as_u64() {
66 write!(f, "{n}")?;
67 } else if let Some(n) = number.as_f64() {
68 debug_assert!(n.ceil() == n, "bad number value");
69 write!(f, "{}", n.ceil() as i64)?;
70 } else if let Some(n) = number.as_i128() {
71 write!(f, "{n}")?;
72 } else if let Some(n) = number.as_u128() {
73 write!(f, "{n}")?;
74 } else {
75 panic!("unable to represent number");
76 }
77 }
78 serde_json::Value::String(s) => {
79 write_shell_escaped_single_quoted(f, &s)?;
80 }
81 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
82 unreachable!("Snix bug: called write_simple_type on complex type")
83 }
84 }
85
86 Ok(())
87}
88
89pub fn write_attrs_sh_file<W>(
92 f: &mut W,
93 map: serde_json::Map<String, serde_json::Value>,
94) -> std::fmt::Result
95where
96 W: std::fmt::Write,
97{
98 for (k, v) in map {
99 if !RE_SH_VAR_NAME.is_match(k.as_str()) {
103 continue;
104 }
105 match v {
106 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => {
107 write!(f, "declare {k}=")?;
108 write_simple_type(f, v)?;
109 writeln!(f)?;
110 }
111 serde_json::Value::Number(_) => {
112 if !is_good_simple_value(&v) {
114 continue;
115 }
116 write!(f, "declare {k}=")?;
117 write_simple_type(f, v)?;
118 writeln!(f)?;
119 }
120 serde_json::Value::Array(values) => {
121 if values.iter().any(|v| !is_good_simple_value(v)) {
123 continue;
124 }
125 write!(f, "declare -a {k}=(")?;
126 for val in values {
127 write_simple_type(f, val)?;
128 write!(f, " ")?;
129 }
130 writeln!(f, ")")?;
131 }
132 serde_json::Value::Object(map) => {
133 if map.values().any(|v| {
136 matches!(
137 v,
138 serde_json::Value::Array(_) | serde_json::Value::Object(_)
139 ) || !is_good_simple_value(v)
140 }) {
141 continue;
142 }
143
144 write!(f, "declare -A {k}=(")?;
145 for (k, v) in map {
146 write!(f, "[")?;
150 write_shell_escaped_single_quoted(f, &k)?;
151 write!(f, "]=")?;
152 write_simple_type(f, v)?;
153 write!(f, " ")?;
154 }
155 writeln!(f, ")")?;
156 }
157 }
158 }
159
160 Ok(())
161}
162
163#[cfg(test)]
164mod test {
165 use rstest::rstest;
166 use serde_json::json;
167
168 use super::write_attrs_sh_file;
169
170 #[rstest]
171 #[case::empty(json!({}), "")]
172 #[case::empty_key(json!({"": "value"}), "")]
173 #[case::null(json!({"k": null}), r#"declare k=''"#)]
174 #[case::string(json!({"k":"v"}), r#"declare k='v'"#)]
175 #[case::string_escaping(json!({"k":"v'w"}), r#"declare k='v'\''w'"#)]
176 #[case::bool_false(json!({"k":false}), r#"declare k="#)]
177 #[case::bool_true(json!({"k":true}), r#"declare k=1"#)]
178 #[case::number(json!({"k":1}), r#"declare k=1"#)]
179 #[case::number_float(json!({"k":1.0}), r#"declare k=1"#)]
180 #[case::number_float_invalid(json!({"k":1.1}), r#""#)]
181 #[case::array_of_strings(json!({"k": ["bar", "baz"]}), r#"declare -a k=('bar' 'baz' )"#)]
182 #[case::array_of_strings_and_bool(json!({"k": ["bar", true]}), r#"declare -a k=('bar' 1 )"#)]
183 #[case::array_of_strings_and_invalid_number(json!({"k": ["bar", 1.1]}), "")]
184 #[case::object_key_escaping(json!(
185 {"k": {"it's": "v"}}), r#"declare -A k=(['it'\''s']='v' )"#)]
186 #[case::object(json!(
187 {"k": {
188 "bar": true,
189 "b": 1.0,
190 "c": false,
191 "d": true,
192 }}), r#"declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )"#)]
193 #[case::object_invalid_number(json!(
194 {"k": {
195 "bar": true,
196 "b": 1.1,
197 }}), "")]
198 #[case::object_too_complex(json!(
199 {"k": {
200 "bar": true,
201 "baz": [],
202 }}), r#""#)]
203 #[case::multiple(json!(
204 {
205 "k": {
206 "bar": true,
207 "b": 1.0,
208 "c": false,
209 "d": true,
210 },
211 "l": 42,
212 "m": false,
213 "n": 1.1,
214 }),
215 r#"declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )
216declare l=42
217declare m="#)]
218 fn write_attrs(#[case] val: serde_json::Value, #[case] exp_output: &str) {
219 let mut out = String::new();
220 let map = val.as_object().expect("must be map").to_owned();
221 write_attrs_sh_file(&mut out, map).expect("must succeed");
222
223 if exp_output.is_empty() {
224 assert_eq!(exp_output, out, "expected output to match");
225 } else {
226 assert_eq!(
227 {
228 let mut exp_output = String::from(exp_output);
229 exp_output.push('\n');
230 exp_output
231 },
232 out,
233 "expected output to match"
234 );
235 }
236 }
237}