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, "['{k}']=")?;
147 write_simple_type(f, v)?;
148 write!(f, " ")?;
149 }
150 writeln!(f, ")")?;
151 }
152 }
153 }
154
155 Ok(())
156}
157
158#[cfg(test)]
159mod test {
160 use rstest::rstest;
161 use serde_json::json;
162
163 use super::write_attrs_sh_file;
164
165 #[rstest]
166 #[case::empty(json!({}), "")]
167 #[case::empty_key(json!({"": "value"}), "")]
168 #[case::null(json!({"k": null}), r#"declare k=''"#)]
169 #[case::string(json!({"k":"v"}), r#"declare k='v'"#)]
170 #[case::string_escaping(json!({"k":"v'"}), r#"declare k='v\''"#)]
171 #[case::bool_false(json!({"k":false}), r#"declare k="#)]
172 #[case::bool_true(json!({"k":true}), r#"declare k=1"#)]
173 #[case::number(json!({"k":1}), r#"declare k=1"#)]
174 #[case::number_float(json!({"k":1.0}), r#"declare k=1"#)]
175 #[case::number_float_invalid(json!({"k":1.1}), r#""#)]
176 #[case::array_of_strings(json!({"k": ["bar", "baz"]}), r#"declare -a k=('bar' 'baz' )"#)]
177 #[case::array_of_strings_and_bool(json!({"k": ["bar", true]}), r#"declare -a k=('bar' 1 )"#)]
178 #[case::array_of_strings_and_invalid_number(json!({"k": ["bar", 1.1]}), "")]
179 #[case::object(json!(
180 {"k": {
181 "bar": true,
182 "b": 1.0,
183 "c": false,
184 "d": true,
185 }}), r#"declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )"#)]
186 #[case::object_invalid_number(json!(
187 {"k": {
188 "bar": true,
189 "b": 1.1,
190 }}), "")]
191 #[case::object_too_complex(json!(
192 {"k": {
193 "bar": true,
194 "baz": [],
195 }}), r#""#)]
196 #[case::multiple(json!(
197 {
198 "k": {
199 "bar": true,
200 "b": 1.0,
201 "c": false,
202 "d": true,
203 },
204 "l": 42,
205 "m": false,
206 "n": 1.1,
207 }),
208 r#"declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )
209declare l=42
210declare m="#)]
211 fn write_attrs(#[case] val: serde_json::Value, #[case] exp_output: &str) {
212 let mut out = String::new();
213 let map = val.as_object().expect("must be map").to_owned();
214 write_attrs_sh_file(&mut out, map).expect("must succeed");
215
216 if exp_output.is_empty() {
217 assert_eq!(exp_output, out, "expected output to match");
218 } else {
219 assert_eq!(
220 {
221 let mut exp_output = String::from(exp_output);
222 exp_output.push('\n');
223 exp_output
224 },
225 out,
226 "expected output to match"
227 );
228 }
229 }
230}