1use super::emit::emit_array;
25use super::error::ParseError;
26use super::parser::{inside, parse_q_words};
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum BashVal {
30 Str(String),
31 Arr(Vec<BashVal>),
32}
33
34#[derive(Debug, Clone, PartialEq)]
35pub enum Schema {
36 Scalar,
37 Arr(Box<Schema>),
38}
39
40impl Schema {
41 pub fn n_d(n: usize) -> Self {
42 let mut schema = Schema::Scalar;
43 for _ in 0..n {
44 schema = Schema::Arr(Box::new(schema));
45 }
46 schema
47 }
48}
49
50impl BashVal {
51 pub fn row(words: impl IntoIterator<Item = impl Into<String>>) -> Self {
53 Self::Arr(
54 words
55 .into_iter()
56 .map(|word| Self::Str(word.into()))
57 .collect(),
58 )
59 }
60
61 pub fn words(self) -> Option<Vec<String>> {
63 let Self::Arr(items) = self else { return None };
64
65 items
66 .into_iter()
67 .map(|item| match item {
68 Self::Str(word) => Some(word),
69 Self::Arr(_) => None,
70 })
71 .collect()
72 }
73
74 pub fn rows(self) -> Option<Vec<Vec<String>>> {
76 let Self::Arr(rows) = self else { return None };
77
78 rows.into_iter().map(Self::words).collect()
79 }
80}
81
82fn scalar_expected(words: &[String]) -> ParseError {
84 ParseError::new(
85 &words.join(" "),
86 0,
87 format!("expected one word, got {}", words.len()),
88 )
89}
90
91pub trait BashCodec {
92 fn emit(&self, val: &BashVal) -> Vec<String>;
95
96 fn parse(&self, words: &[String], schema: &Schema) -> Result<BashVal, ParseError>;
99
100 fn emit_literal(&self, val: &BashVal) -> String {
102 emit_array(&self.emit(val))
103 }
104
105 fn parse_literal(&self, input: &str, schema: &Schema) -> Result<BashVal, ParseError> {
106 self.parse(&parse_q_words(inside(input)?)?, schema)
107 }
108
109 fn rows(&self, input: &str) -> Result<Vec<Vec<String>>, ParseError> {
113 self.parse_literal(input, &Schema::n_d(2))?
114 .rows()
115 .ok_or_else(|| ParseError::new(input, 0, "expected rows"))
116 }
117}
118
119pub struct QuotedNest;
120
121impl BashCodec for QuotedNest {
122 fn emit(&self, val: &BashVal) -> Vec<String> {
123 match val {
124 BashVal::Str(word) => vec![word.clone()],
125 BashVal::Arr(items) => items
126 .iter()
127 .map(|item| match item {
128 BashVal::Str(word) => word.clone(),
129 BashVal::Arr(_) => self.emit_literal(item),
130 })
131 .collect(),
132 }
133 }
134
135 fn parse(&self, words: &[String], schema: &Schema) -> Result<BashVal, ParseError> {
136 match schema {
137 Schema::Scalar => match words {
138 [only] => Ok(BashVal::Str(only.clone())),
139 _ => Err(scalar_expected(words)),
140 },
141 Schema::Arr(inner) => words
142 .iter()
143 .map(|word| match **inner {
144 Schema::Scalar => Ok(BashVal::Str(word.clone())),
145 Schema::Arr(_) => self.parse_literal(word, inner),
146 })
147 .collect::<Result<_, _>>()
148 .map(BashVal::Arr),
149 }
150 }
151}
152
153pub struct LinkedArr;
167
168impl BashCodec for LinkedArr {
169 fn emit(&self, val: &BashVal) -> Vec<String> {
170 match val {
171 BashVal::Str(word) => vec![word.clone()],
172 BashVal::Arr(items) => {
173 let nested = matches!(items.first(), Some(BashVal::Arr(_)));
174 let mut out = Vec::new();
175 for item in items {
176 let body = self.emit(item);
177 if nested {
178 out.push(body.len().to_string());
179 }
180 out.extend(body);
181 }
182 out
183 }
184 }
185 }
186
187 fn parse(&self, words: &[String], schema: &Schema) -> Result<BashVal, ParseError> {
188 match schema {
189 Schema::Scalar => match words {
190 [only] => Ok(BashVal::Str(only.clone())),
191 _ => Err(scalar_expected(words)),
192 },
193 Schema::Arr(_) => {
194 let (val, consumed) = parse_body(words, schema)?;
195 if consumed != words.len() {
196 return Err(ParseError::new(
197 &words.join(" "),
198 0,
199 format!(
200 "trailing words: consumed {consumed} of {}",
201 words.len()
202 ),
203 ));
204 }
205 Ok(val)
206 }
207 }
208 }
209}
210
211fn parse_body(words: &[String], schema: &Schema) -> Result<(BashVal, usize), ParseError> {
212 let Schema::Arr(inner) = schema else {
213 return match words.first() {
214 Some(word) => Ok((BashVal::Str(word.clone()), 1)),
215 None => Err(ParseError::new(
216 "",
217 0,
218 "a scalar position with no word",
219 )),
220 };
221 };
222
223 let grouped = matches!(**inner, Schema::Arr(_));
224 let mut items = Vec::new();
225 let mut at = 0;
226
227 while at < words.len() {
228 if !grouped {
229 items.push(BashVal::Str(words[at].clone()));
230 at += 1;
231 continue;
232 }
233
234 let width: usize = words[at].parse().map_err(|_| {
235 ParseError::new(
236 &words.join(" "),
237 0,
238 format!(
239 "length prefix not numeric at pos {at}: {:?}",
240 words[at]
241 ),
242 )
243 })?;
244 at += 1;
245
246 let end = at + width;
247 if end > words.len() {
248 return Err(ParseError::new(
249 &words.join(" "),
250 0,
251 format!(
252 "group claims {width} words; only {} available",
253 words.len() - at
254 ),
255 ));
256 }
257
258 let (item, consumed) = parse_body(&words[at..end], inner)?;
259 if consumed != end - at {
260 return Err(ParseError::new(
261 &words.join(" "),
262 0,
263 format!(
264 "nested group: consumed {consumed} of {} body words",
265 end - at
266 ),
267 ));
268 }
269 items.push(item);
270 at = end;
271 }
272
273 Ok((BashVal::Arr(items), at))
274}
275
276#[cfg(test)]
277mod tests {
278
279 #[test]
282 fn rows_round_trip_through_one_flat_array() {
283 let rows = vec![
284 vec!["AspectRequire".to_string(), "env".into(), "mod a".into()],
285 vec!["Accumulate".to_string()],
286 Vec::new(),
287 ];
288
289 let text = QuotedNest.emit_literal(&BashVal::Arr(
290 rows.iter()
291 .map(|row| BashVal::row(row.iter().cloned()))
292 .collect(),
293 ));
294 let outer = crate::parse_array(&text).unwrap();
295
296 assert_eq!(
297 outer.len(),
298 3,
299 "three words at the outer level, one per row"
300 );
301 assert_eq!(
302 outer[0], "('AspectRequire' 'env' 'mod a')",
303 "each one an array literal"
304 );
305 assert_eq!(
306 crate::parse_array(&outer[0]).unwrap(),
307 rows[0],
308 "which reads back on its own"
309 );
310
311 assert_eq!(
312 QuotedNest.rows(&text).unwrap(),
313 rows,
314 "or in one step"
315 );
316 }
317 use super::*;
318
319 fn row(words: &[&str]) -> BashVal {
320 BashVal::row(words.iter().copied())
321 }
322
323 fn arr(items: Vec<BashVal>) -> BashVal {
324 BashVal::Arr(items)
325 }
326
327 fn words(items: &[&str]) -> Vec<String> {
328 items.iter().map(|word| word.to_string()).collect()
329 }
330
331 #[test]
334 fn quoted_nest_wraps_a_level_per_dimension() {
335 let two_d = arr(vec![
336 row(&["a", "b"]),
337 row(&["c", "d", "e"]),
338 ]);
339
340 assert_eq!(
341 QuotedNest.emit(&two_d),
342 words(&["('a' 'b')", "('c' 'd' 'e')"])
343 );
344 assert_eq!(
345 QuotedNest
346 .parse(
347 &QuotedNest.emit(&two_d),
348 &Schema::n_d(2)
349 )
350 .unwrap(),
351 two_d
352 );
353 }
354
355 #[test]
356 fn linked_arr_prefixes_each_group_with_its_width() {
357 assert_eq!(
358 LinkedArr.emit(&arr(vec![
359 row(&["a", "b"]),
360 row(&["c", "d", "e"])
361 ])),
362 words(&["2", "a", "b", "3", "c", "d", "e"])
363 );
364 assert_eq!(
365 LinkedArr.emit(&arr(vec![arr(vec![
366 row(&["a", "b"]),
367 row(&["c"])
368 ])])),
369 words(&["5", "2", "a", "b", "1", "c"])
370 );
371 assert_eq!(
372 LinkedArr.emit(&arr(vec![
373 arr(vec![row(&["a", "b"])]),
374 arr(vec![row(&["c"])])
375 ])),
376 words(&["3", "2", "a", "b", "2", "1", "c"])
377 );
378 }
379
380 #[test]
383 fn quoted_nest_round_trips_at_three_dimensions() {
384 let three_d = arr(vec![
385 arr(vec![row(&["a", "b"]), row(&["c"])]),
386 arr(vec![row(&["d", "e"])]),
387 ]);
388 let text = QuotedNest.emit_literal(&three_d);
389
390 assert_eq!(
391 QuotedNest.parse_literal(&text, &Schema::n_d(3)).unwrap(),
392 three_d
393 );
394 assert_eq!(
395 QuotedNest
396 .parse_literal(&text, &Schema::n_d(2))
397 .unwrap()
398 .rows()
399 .unwrap()
400 .len(),
401 2,
402 "read one level shallower it is still two rows, of one word each"
403 );
404 }
405
406 #[test]
407 fn linked_arr_round_trips_at_three_dimensions() {
408 let three_d = arr(vec![
409 arr(vec![row(&["a", "b"]), row(&["c"])]),
410 arr(vec![row(&["d", "e"])]),
411 ]);
412
413 assert_eq!(
414 LinkedArr
415 .parse(
416 &LinkedArr.emit(&three_d),
417 &Schema::n_d(3)
418 )
419 .unwrap(),
420 three_d
421 );
422 }
423}