1use std::io::Read;
2use std::str::FromStr;
3
4use serde_json::{Map, Number, Value};
5
6use crate::error::ToonifyError;
7use crate::options::{DecoderOptions, Delimiter, PathExpansionMode};
8use crate::quoting::is_identifier_segment;
9
10pub fn decode_str(input: &str, options: DecoderOptions) -> Result<Value, ToonifyError> {
12 let mut decoder = Decoder::new(input, options)?;
13 let mut value = decoder.parse_root()?;
14
15 if matches!(decoder.options.expand_paths, PathExpansionMode::Safe) {
16 value = expand_paths(value, decoder.options.strict)?;
17 }
18
19 Ok(value)
20}
21
22pub fn decode_reader<R: Read>(
24 mut reader: R,
25 options: DecoderOptions,
26) -> Result<Value, ToonifyError> {
27 let mut buf = String::new();
28 reader.read_to_string(&mut buf)?;
29 decode_str(&buf, options)
30}
31
32struct Decoder {
33 lines: Vec<Line>,
34 index: usize,
35 options: DecoderOptions,
36}
37
38#[derive(Clone, Debug)]
39struct Line {
40 depth: usize,
41 text: String,
42 number: usize,
43}
44
45impl Decoder {
46 fn new(input: &str, options: DecoderOptions) -> Result<Self, ToonifyError> {
47 let mut lines = Vec::new();
48 for (idx, raw) in input.lines().enumerate() {
49 let line_number = idx + 1;
50 if raw.trim().is_empty() {
51 continue;
52 }
53
54 let mut indent_chars = 0usize;
55 for ch in raw.chars() {
56 match ch {
57 ' ' => indent_chars += 1,
58 '\t' => {
59 return Err(ToonifyError::decoding(format!(
60 "line {line_number}: tabs are not allowed for indentation"
61 )))
62 }
63 _ => break,
64 }
65 }
66
67 if indent_chars % options.indent != 0 {
68 return Err(ToonifyError::decoding(format!(
69 "line {line_number}: indentation must be a multiple of {} spaces",
70 options.indent
71 )));
72 }
73
74 let depth = indent_chars / options.indent;
75 let text = raw[indent_chars..].trim_end();
76 if text.is_empty() {
77 continue;
78 }
79
80 lines.push(Line {
81 depth,
82 text: text.to_string(),
83 number: line_number,
84 });
85 }
86
87 Ok(Self {
88 lines,
89 index: 0,
90 options,
91 })
92 }
93
94 fn parse_root(&mut self) -> Result<Value, ToonifyError> {
95 if self.lines.is_empty() {
96 return Ok(Value::Object(Map::new()));
97 }
98
99 if self.lines[0].text.starts_with('[') {
100 let header = self
101 .parse_header_for_line(&self.lines[0], false)?
102 .ok_or_else(|| {
103 ToonifyError::decoding(format!(
104 "line {}: expected array header",
105 self.lines[0].number
106 ))
107 })?;
108 self.index += 1;
109 return self.consume_array(header, 0);
110 }
111
112 if !self.lines[0].text.contains(':') {
113 let value = parse_primitive_token(self.lines[0].text.trim()).map_err(|err| {
114 ToonifyError::decoding(format!("line {}: {err}", self.lines[0].number))
115 })?;
116 self.index = self.lines.len();
117 return Ok(value);
118 }
119
120 let object = self.parse_object(0)?;
121 Ok(Value::Object(object))
122 }
123
124 fn parse_object(&mut self, depth: usize) -> Result<Map<String, Value>, ToonifyError> {
125 let mut map = Map::new();
126 while let Some(line) = self.peek_line().cloned() {
127 if line.depth != depth {
128 break;
129 }
130
131 if let Some(header) = self.try_parse_header(&line, true)? {
132 self.index += 1;
133 let key = header.key.clone().ok_or_else(|| {
134 ToonifyError::decoding(format!(
135 "line {}: array header requires a key",
136 line.number
137 ))
138 })?;
139 let value = self.consume_array(header, depth)?;
140 map.insert(key, value);
141 continue;
142 }
143
144 self.consume_field(&mut map, depth)?;
145 }
146 Ok(map)
147 }
148
149 fn consume_field(
150 &mut self,
151 map: &mut Map<String, Value>,
152 depth: usize,
153 ) -> Result<(), ToonifyError> {
154 let line = self
155 .peek_line()
156 .cloned()
157 .ok_or_else(|| ToonifyError::decoding("unexpected end of document"))?;
158
159 if let Some(header) = self.parse_header_for_line(&line, true)? {
160 self.index += 1;
161 let key = header.key.clone().ok_or_else(|| {
162 ToonifyError::decoding(format!("line {}: array header requires a key", line.number))
163 })?;
164 let value = self.consume_array(header, depth)?;
165 map.insert(key, value);
166 return Ok(());
167 }
168
169 let (raw_key, rest) = split_key_value(&line.text).ok_or_else(|| {
170 ToonifyError::decoding(format!("line {}: expected `key: value`", line.number))
171 })?;
172 let key = parse_key_token(raw_key)
173 .map_err(|err| ToonifyError::decoding(format!("line {}: {err}", line.number)))?;
174
175 self.index += 1;
176
177 if rest.trim().is_empty() {
178 if let Some(next) = self.peek_line() {
180 if next.depth <= depth {
181 map.insert(key, Value::Object(Map::new()));
182 return Ok(());
183 }
184 } else {
185 map.insert(key, Value::Object(Map::new()));
186 return Ok(());
187 }
188
189 let value = self.parse_value_block(depth + 1)?;
190 map.insert(key, value);
191 return Ok(());
192 }
193
194 let value = parse_primitive_token(rest.trim())
195 .map_err(|err| ToonifyError::decoding(format!("line {}: {err}", line.number)))?;
196 map.insert(key, value);
197 Ok(())
198 }
199
200 fn parse_value_block(&mut self, depth: usize) -> Result<Value, ToonifyError> {
201 if let Some(line) = self.peek_line() {
202 if line.depth != depth {
203 return Ok(Value::Object(Map::new()));
204 }
205
206 if line.text.starts_with('[') {
207 let header = self.parse_header_for_line(line, false)?.ok_or_else(|| {
208 ToonifyError::decoding(format!("line {}: expected array header", line.number))
209 })?;
210 self.index += 1;
211 return self.consume_array(header, depth - 1);
212 }
213
214 if split_key_value(&line.text).is_some() {
215 let object = self.parse_object(depth)?;
216 return Ok(Value::Object(object));
217 }
218
219 let value = parse_primitive_token(line.text.trim())
220 .map_err(|err| ToonifyError::decoding(format!("line {}: {err}", line.number)))?;
221 self.index += 1;
222 return Ok(value);
223 }
224
225 Ok(Value::Null)
226 }
227
228 fn try_parse_header(
229 &self,
230 line: &Line,
231 expect_key: bool,
232 ) -> Result<Option<ArrayHeader>, ToonifyError> {
233 if !line.text.contains('[') {
234 return Ok(None);
235 }
236
237 if let Some(header) = self.parse_header_for_line(line, expect_key)? {
238 return Ok(Some(header));
239 }
240
241 Ok(None)
242 }
243
244 fn parse_header_for_line(
245 &self,
246 line: &Line,
247 expect_key: bool,
248 ) -> Result<Option<ArrayHeader>, ToonifyError> {
249 parse_header(&line.text, expect_key, line.number)
250 }
251
252 fn consume_array(
253 &mut self,
254 header: ArrayHeader,
255 container_depth: usize,
256 ) -> Result<Value, ToonifyError> {
257 if let Some(inline) = header
258 .inline_values
259 .as_deref()
260 .filter(|value| !value.is_empty())
261 {
262 return self.parse_inline_array(header.len, header.delimiter, inline, header.line);
263 }
264
265 if header.fields.is_some() {
266 return self.parse_tabular_array(header, container_depth);
267 }
268
269 self.parse_list_array(header, container_depth)
270 }
271
272 fn parse_inline_array(
273 &self,
274 len: usize,
275 delimiter: Delimiter,
276 values: &str,
277 line: usize,
278 ) -> Result<Value, ToonifyError> {
279 let cells = split_delimited(values, delimiter)?;
280 if self.options.strict && cells.len() != len {
281 return Err(ToonifyError::decoding(format!(
282 "line {line}: expected {len} values but found {}",
283 cells.len()
284 )));
285 }
286
287 let mut out = Vec::with_capacity(cells.len());
288 for cell in cells {
289 let value = parse_primitive_token(cell.trim())
290 .map_err(|err| ToonifyError::decoding(format!("line {line}: {err}")))?;
291 out.push(value);
292 }
293 Ok(Value::Array(out))
294 }
295
296 fn parse_tabular_array(
297 &mut self,
298 header: ArrayHeader,
299 container_depth: usize,
300 ) -> Result<Value, ToonifyError> {
301 let fields = header.fields.clone().unwrap_or_default();
302 let row_depth = container_depth + 1;
303 let mut rows = Vec::new();
304
305 while let Some(line) = self.peek_line().cloned() {
306 if line.depth != row_depth {
307 break;
308 }
309
310 if !is_tabular_row_line(&line.text, header.delimiter) {
311 break;
312 }
313
314 let cells = split_delimited(&line.text, header.delimiter)?;
315 if self.options.strict && cells.len() != fields.len() {
316 return Err(ToonifyError::decoding(format!(
317 "line {}: expected {} cells but found {}",
318 line.number,
319 fields.len(),
320 cells.len()
321 )));
322 }
323
324 let mut map = Map::new();
325 for (idx, field) in fields.iter().enumerate() {
326 let cell = cells.get(idx).map(|s| s.trim()).unwrap_or("");
327 let value = parse_primitive_token(cell).map_err(|err| {
328 ToonifyError::decoding(format!("line {}: {err}", line.number))
329 })?;
330 map.insert(field.clone(), value);
331 }
332
333 rows.push(Value::Object(map));
334 self.index += 1;
335 }
336
337 if self.options.strict && rows.len() != header.len {
338 return Err(ToonifyError::decoding(format!(
339 "line {}: expected {} rows but found {}",
340 header.line,
341 header.len,
342 rows.len()
343 )));
344 }
345
346 Ok(Value::Array(rows))
347 }
348
349 fn parse_list_array(
350 &mut self,
351 header: ArrayHeader,
352 container_depth: usize,
353 ) -> Result<Value, ToonifyError> {
354 let row_depth = container_depth + 1;
355 let mut items = Vec::new();
356
357 while let Some(line) = self.peek_line().cloned() {
358 if line.depth != row_depth {
359 break;
360 }
361
362 if !line.text.starts_with("- ") {
363 return Err(ToonifyError::decoding(format!(
364 "line {}: expected '-' to start list item",
365 line.number
366 )));
367 }
368
369 let remainder = line.text[2..].trim();
370 self.index += 1;
371
372 let value = if remainder.is_empty() {
373 let object = self.parse_object(row_depth + 1)?;
374 Value::Object(object)
375 } else if let Some(sub_header) = parse_header(remainder, false, line.number)? {
376 let key = sub_header.key.clone();
377 let value = self.consume_nested_header(sub_header, row_depth)?;
378 if let Some(key) = key {
379 let mut map = Map::new();
380 map.insert(key, value);
381 while let Some(next) = self.peek_line() {
382 if next.depth != row_depth + 1 {
383 break;
384 }
385 self.consume_field(&mut map, row_depth + 1)?;
386 }
387 Value::Object(map)
388 } else {
389 value
390 }
391 } else if remainder.contains(':') {
392 self.parse_inline_object_in_list(remainder, row_depth, line.number)?
393 } else {
394 parse_primitive_token(remainder)
395 .map_err(|err| ToonifyError::decoding(format!("line {}: {err}", line.number)))?
396 };
397
398 items.push(value);
399 }
400
401 if self.options.strict && items.len() != header.len {
402 return Err(ToonifyError::decoding(format!(
403 "line {}: expected {} list items but found {}",
404 header.line,
405 header.len,
406 items.len()
407 )));
408 }
409
410 Ok(Value::Array(items))
411 }
412
413 fn consume_nested_header(
414 &mut self,
415 mut header: ArrayHeader,
416 row_depth: usize,
417 ) -> Result<Value, ToonifyError> {
418 header.key = None;
420 self.consume_array(header, row_depth)
421 }
422
423 fn parse_inline_object_in_list(
424 &mut self,
425 inline: &str,
426 row_depth: usize,
427 line_number: usize,
428 ) -> Result<Value, ToonifyError> {
429 let (raw_key, rest) = split_key_value(inline).ok_or_else(|| {
430 ToonifyError::decoding(format!("line {line_number}: invalid list object syntax"))
431 })?;
432 let key = parse_key_token(raw_key)
433 .map_err(|err| ToonifyError::decoding(format!("line {line_number}: {err}")))?;
434
435 let mut map = Map::new();
436 if rest.trim().is_empty() {
437 let value = self.parse_value_block(row_depth + 2)?;
438 map.insert(key, value);
439 } else {
440 let value = parse_primitive_token(rest.trim())
441 .map_err(|err| ToonifyError::decoding(format!("line {line_number}: {err}")))?;
442 map.insert(key, value);
443 }
444
445 while let Some(next) = self.peek_line() {
446 if next.depth != row_depth + 1 {
447 break;
448 }
449 self.consume_field(&mut map, row_depth + 1)?;
450 }
451
452 Ok(Value::Object(map))
453 }
454
455 fn peek_line(&self) -> Option<&Line> {
456 self.lines.get(self.index)
457 }
458}
459
460#[derive(Clone, Debug)]
461struct ArrayHeader {
462 key: Option<String>,
463 len: usize,
464 delimiter: Delimiter,
465 fields: Option<Vec<String>>,
466 inline_values: Option<String>,
467 line: usize,
468}
469
470fn parse_header(
471 text: &str,
472 expect_key: bool,
473 line: usize,
474) -> Result<Option<ArrayHeader>, ToonifyError> {
475 let colon_idx = match text.find(':') {
476 Some(idx) => idx,
477 None => return Ok(None),
478 };
479
480 let before = text[..colon_idx].trim_end();
481 let after = text[colon_idx + 1..].trim_start();
482
483 if !before.contains('[') {
484 return Ok(None);
485 }
486
487 let bracket_idx = before
488 .rfind('[')
489 .ok_or_else(|| ToonifyError::decoding(format!("line {line}: malformed array header")))?;
490
491 let (raw_key, bracket_part) = if bracket_idx == 0 {
492 (None, before)
493 } else {
494 let key_text = before[..bracket_idx].trim_end();
495 let key = parse_key_token(key_text)
496 .map_err(|err| ToonifyError::decoding(format!("line {line}: {err}")))?;
497 (Some(key), &before[bracket_idx..])
498 };
499
500 if expect_key && raw_key.is_none() {
501 return Err(ToonifyError::decoding(format!(
502 "line {line}: array header must include a key"
503 )));
504 }
505
506 let closing = bracket_part
507 .find(']')
508 .ok_or_else(|| ToonifyError::decoding(format!("line {line}: missing closing ']'")))?;
509
510 let mut bracket_inner = bracket_part[1..closing].trim();
511 let delimiter = if bracket_inner.ends_with('|') {
512 bracket_inner = &bracket_inner[..bracket_inner.len() - 1];
513 Delimiter::Pipe
514 } else if bracket_inner.ends_with('\t') {
515 bracket_inner = &bracket_inner[..bracket_inner.len() - 1];
516 Delimiter::Tab
517 } else {
518 Delimiter::Comma
519 };
520
521 let len: usize = bracket_inner
522 .parse()
523 .map_err(|_| ToonifyError::decoding(format!("line {line}: invalid array length")))?;
524
525 let mut remainder = bracket_part[closing + 1..].trim_start();
526 let fields = if remainder.starts_with('{') {
527 let closing_brace = remainder.find('}').ok_or_else(|| {
528 ToonifyError::decoding(format!("line {line}: missing '}}' in field list"))
529 })?;
530 let field_segment = &remainder[1..closing_brace];
531 let list = parse_field_list(field_segment, delimiter)?;
532 remainder = remainder[closing_brace + 1..].trim_start();
533 Some(list)
534 } else {
535 None
536 };
537
538 if !remainder.is_empty() {
539 return Err(ToonifyError::decoding(format!(
540 "line {line}: unexpected content after array header"
541 )));
542 }
543
544 Ok(Some(ArrayHeader {
545 key: raw_key,
546 len,
547 delimiter,
548 fields,
549 inline_values: if after.is_empty() {
550 None
551 } else {
552 Some(after.to_string())
553 },
554 line,
555 }))
556}
557
558fn parse_field_list(segment: &str, delimiter: Delimiter) -> Result<Vec<String>, ToonifyError> {
559 let mut fields = Vec::new();
560 for raw in split_delimited(segment, delimiter)? {
561 let key = parse_key_token(raw.trim())
562 .map_err(|err| ToonifyError::decoding(format!("invalid field name: {err}")))?;
563 fields.push(key);
564 }
565 Ok(fields)
566}
567
568fn split_key_value(text: &str) -> Option<(&str, &str)> {
569 let mut in_quotes = false;
570 let mut escaped = false;
571 for (idx, ch) in text.char_indices() {
572 match ch {
573 '"' if !escaped => in_quotes = !in_quotes,
574 '\\' if in_quotes => {
575 escaped = !escaped;
576 continue;
577 }
578 ':' if !in_quotes => {
579 let key = text[..idx].trim_end();
580 let value = text[idx + 1..].trim_start();
581 return Some((key, value));
582 }
583 _ => {}
584 }
585 escaped = false;
586 }
587 None
588}
589
590fn parse_key_token(raw: &str) -> Result<String, String> {
591 if raw.starts_with('"') {
592 return parse_quoted_string(raw);
593 }
594 if raw.is_empty() {
595 return Err("key cannot be empty".into());
596 }
597 Ok(raw.to_string())
598}
599
600fn parse_quoted_string(raw: &str) -> Result<String, String> {
601 if !raw.ends_with('"') {
602 return Err("unterminated string".into());
603 }
604 let inner = &raw[1..raw.len() - 1];
605 let mut chars = inner.chars();
606 let mut out = String::with_capacity(inner.len());
607 while let Some(ch) = chars.next() {
608 if ch == '\\' {
609 let escaped = chars
610 .next()
611 .ok_or_else(|| "unterminated escape".to_string())?;
612 match escaped {
613 '\\' => out.push('\\'),
614 '"' => out.push('"'),
615 'n' => out.push('\n'),
616 'r' => out.push('\r'),
617 't' => out.push('\t'),
618 other => {
619 return Err(format!("unsupported escape \\{other}"));
620 }
621 }
622 } else {
623 out.push(ch);
624 }
625 }
626 Ok(out)
627}
628
629fn parse_primitive_token(token: &str) -> Result<Value, String> {
630 if token.starts_with('"') {
631 return parse_quoted_string(token).map(Value::String);
632 }
633
634 match token {
635 "true" => return Ok(Value::Bool(true)),
636 "false" => return Ok(Value::Bool(false)),
637 "null" => return Ok(Value::Null),
638 _ => {}
639 }
640
641 if is_numeric_literal(token) {
642 let number = Number::from_str(token).map_err(|_| "invalid number literal".to_string())?;
643 return Ok(Value::Number(number));
644 }
645
646 Ok(Value::String(token.to_string()))
647}
648
649fn is_numeric_literal(token: &str) -> bool {
650 if token.is_empty() {
651 return false;
652 }
653 if token.starts_with('0') && token.len() > 1 && token.chars().all(|c| c.is_ascii_digit()) {
654 return false;
655 }
656 Number::from_str(token).is_ok()
657}
658
659fn split_delimited(input: &str, delimiter: Delimiter) -> Result<Vec<String>, ToonifyError> {
660 let separator = delimiter.as_char();
661 let mut values = Vec::new();
662 let mut current = String::new();
663 let mut in_quotes = false;
664 let mut chars = input.chars().peekable();
665 while let Some(ch) = chars.next() {
666 match ch {
667 '"' => {
668 current.push(ch);
669 in_quotes = !in_quotes;
670 }
671 '\\' if in_quotes => {
672 current.push(ch);
673 if let Some(next) = chars.next() {
674 current.push(next);
675 }
676 }
677 _ if !in_quotes && ch == separator => {
678 values.push(current.trim().to_string());
679 current.clear();
680 }
681 _ => current.push(ch),
682 }
683 }
684 values.push(current.trim().to_string());
685 Ok(values)
686}
687
688fn is_tabular_row_line(text: &str, delimiter: Delimiter) -> bool {
689 let mut first_delim = None;
690 let mut first_colon = None;
691 let mut in_quotes = false;
692 let mut escaped = false;
693 let separator = delimiter.as_char();
694
695 for (idx, ch) in text.char_indices() {
696 if in_quotes {
697 if escaped {
698 escaped = false;
699 continue;
700 }
701 match ch {
702 '\\' => {
703 escaped = true;
704 }
705 '"' => in_quotes = false,
706 _ => {}
707 }
708 continue;
709 }
710
711 match ch {
712 '"' => in_quotes = true,
713 ':' => {
714 if first_colon.is_none() {
715 first_colon = Some(idx);
716 }
717 }
718 other if other == separator => {
719 if first_delim.is_none() {
720 first_delim = Some(idx);
721 }
722 }
723 _ => {}
724 }
725
726 if first_delim.is_some() && first_colon.is_some() {
727 break;
728 }
729 }
730
731 match (first_delim, first_colon) {
732 (None, None) => true,
733 (None, Some(_)) => false,
734 (Some(_), None) => true,
735 (Some(delim_idx), Some(colon_idx)) => delim_idx < colon_idx,
736 }
737}
738
739fn expand_paths(value: Value, strict: bool) -> Result<Value, ToonifyError> {
740 match value {
741 Value::Object(map) => {
742 let mut replacement = Map::new();
743 for (key, val) in map {
744 let val = expand_paths(val, strict)?;
745 if key.contains('.') && key.split('.').all(is_identifier_segment) {
746 insert_expanded(&mut replacement, &key, val, strict)?;
747 } else {
748 replacement.insert(key, val);
749 }
750 }
751 Ok(Value::Object(replacement))
752 }
753 Value::Array(items) => {
754 let mut out = Vec::with_capacity(items.len());
755 for item in items {
756 out.push(expand_paths(item, strict)?);
757 }
758 Ok(Value::Array(out))
759 }
760 other => Ok(other),
761 }
762}
763
764fn insert_expanded(
765 target: &mut Map<String, Value>,
766 dotted: &str,
767 value: Value,
768 strict: bool,
769) -> Result<(), ToonifyError> {
770 let segments: Vec<&str> = dotted.split('.').collect();
771 if segments.is_empty() {
772 return Ok(());
773 }
774 insert_segments(target, &segments, value, strict, dotted)
775}
776
777fn insert_segments(
778 current: &mut Map<String, Value>,
779 segments: &[&str],
780 value: Value,
781 strict: bool,
782 full_key: &str,
783) -> Result<(), ToonifyError> {
784 if segments.len() == 1 {
785 match current.get_mut(segments[0]) {
786 Some(existing) => {
787 if strict {
788 return Err(ToonifyError::decoding(format!(
789 "expansion conflict at '{full_key}'"
790 )));
791 }
792 *existing = value;
793 }
794 None => {
795 current.insert(segments[0].to_string(), value);
796 }
797 }
798 return Ok(());
799 }
800
801 let entry = current
802 .entry(segments[0].to_string())
803 .or_insert_with(|| Value::Object(Map::new()));
804
805 match entry {
806 Value::Object(map) => insert_segments(map, &segments[1..], value, strict, full_key),
807 other => {
808 if strict {
809 Err(ToonifyError::decoding(format!(
810 "expansion conflict at '{full_key}': expected object but found {other:?}"
811 )))
812 } else {
813 *other = Value::Object(Map::new());
814 if let Value::Object(map) = other {
815 insert_segments(map, &segments[1..], value, strict, full_key)
816 } else {
817 unreachable!()
818 }
819 }
820 }
821 }
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use serde_json::json;
828
829 #[test]
830 fn decodes_list_item_with_nested_object_first_field() {
831 let doc = r#"items[1]:
832 - user:
833 name: Ada
834 email: ada@example.com
835 role: admin
836"#;
837
838 let value = decode_str(doc, DecoderOptions::default()).unwrap();
839 let expected = json!({
840 "items": [
841 {
842 "user": {
843 "name": "Ada",
844 "email": "ada@example.com"
845 },
846 "role": "admin"
847 }
848 ]
849 });
850 assert_eq!(value, expected);
851 }
852
853 #[test]
854 fn decodes_tabular_array_on_hyphen_line_and_resumes_fields() {
855 let doc = r#"groups[1]:
856 - members[2]{id,name}:
857 1,Ada
858 2,Bob
859 status: active
860"#;
861
862 let value = decode_str(doc, DecoderOptions::default()).unwrap();
863 let expected = json!({
864 "groups": [
865 {
866 "members": [
867 { "id": 1, "name": "Ada" },
868 { "id": 2, "name": "Bob" }
869 ],
870 "status": "active"
871 }
872 ]
873 });
874 assert_eq!(value, expected);
875 }
876
877 #[test]
878 fn decodes_inline_array_field_inside_object() {
879 let doc = r#"form:
880 op[2]: readproperty,writeproperty
881"#;
882
883 let value = decode_str(doc, DecoderOptions::default()).unwrap();
884 let expected = json!({
885 "form": {
886 "op": ["readproperty", "writeproperty"]
887 }
888 });
889 assert_eq!(value, expected);
890 }
891}