1
// @@ begin test lint list maintained by maint/add_warning @@
2
#![allow(clippy::bool_assert_comparison)]
3
#![allow(clippy::clone_on_copy)]
4
#![allow(clippy::dbg_macro)]
5
#![allow(clippy::mixed_attributes_style)]
6
#![allow(clippy::print_stderr)]
7
#![allow(clippy::print_stdout)]
8
#![allow(clippy::single_char_pattern)]
9
#![allow(clippy::unwrap_used)]
10
#![allow(clippy::unchecked_time_subtraction)]
11
#![allow(clippy::useless_vec)]
12
#![allow(clippy::needless_pass_by_value)]
13
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
14
#![allow(clippy::needless_borrows_for_generic_args)] // TODO add to maint/add_warning
15

            
16
use std::fmt::{self, Debug};
17
use std::mem;
18
use std::slice;
19

            
20
use anyhow::Context as _;
21
use derive_deftly::Deftly;
22
use itertools::{Itertools, chain};
23
use testresult::TestResult;
24
use tor_error::{Bug, ErrorReport as _};
25

            
26
use crate::encode::{ItemEncoder, ItemObjectEncodable, NetdocEncodable, NetdocEncoder};
27
use crate::parse2::{
28
    ArgumentError as P2AE, ArgumentStream, ErrorProblem as P2EP, ItemObjectParseable,
29
    NetdocParseable, ParseError, ParseInput, UnparsedItem, parse_netdoc, parse_netdoc_multiple,
30
    parse_netdoc_multiple_with_offsets,
31
};
32
use crate::types::{Ignored, NotPresent};
33

            
34
34
fn default<T: Default>() -> T {
35
34
    Default::default()
36
34
}
37

            
38
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
39
#[derive_deftly(NetdocEncodable, NetdocParseable)]
40
struct Top {
41
    top_intro: (),
42
    needed: (String,),
43
    optional: Option<(String,)>,
44
    several: Vec<(String,)>,
45
    not_present: NotPresent,
46
    #[deftly(netdoc(default))]
47
    defaulted: (i32,),
48
    #[deftly(netdoc(keyword = "renamed"))]
49
    t4_renamed: Option<(String,)>,
50
    #[deftly(netdoc(subdoc))]
51
    sub1: Sub1,
52
    #[deftly(netdoc(subdoc))]
53
    sub2: Option<Sub2>,
54
    #[deftly(netdoc(subdoc))]
55
    sub3: Vec<Sub3>,
56
    #[deftly(netdoc(subdoc, default))]
57
    sub4: Sub4,
58
}
59

            
60
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
61
#[derive_deftly(NetdocEncodable, NetdocParseable)]
62
struct Sub1 {
63
    sub1_intro: (),
64
    sub1_field: Option<(String,)>,
65
    #[deftly(netdoc(flatten))]
66
    flatten: Flat1,
67
}
68
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
69
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
70
struct Flat1 {
71
    flat_needed: (String,),
72
    flat_optional: Option<(String,)>,
73
    flat_several: Vec<(String,)>,
74
    flat_defaulted: Option<(String,)>,
75
    #[deftly(netdoc(single_arg))]
76
    flat_arg_needed: String,
77
    #[deftly(netdoc(single_arg))]
78
    flat_arg_optional: Option<String>,
79
    #[deftly(netdoc(single_arg))]
80
    flat_arg_several: Vec<String>,
81
    #[deftly(netdoc(single_arg, default))]
82
    flat_arg_defaulted: i32,
83
    #[deftly(netdoc(with = "needs_with_parse"))]
84
    flat_with_needed: NeedsWith,
85
    #[deftly(netdoc(with = "needs_with_parse"))]
86
    flat_with_optional: Option<NeedsWith>,
87
    #[deftly(netdoc(with = "needs_with_parse"))]
88
    flat_with_several: Vec<NeedsWith>,
89
    #[deftly(netdoc(flatten))]
90
    flat_flat: FlatInner,
91
}
92
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
93
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
94
struct FlatInner {
95
    flat_inner_optional: Option<(String,)>,
96
}
97
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
98
#[derive_deftly(NetdocEncodable, NetdocParseable)]
99
struct Sub2 {
100
    #[deftly(netdoc(with = "needs_with_intro"))]
101
    sub2_intro: NeedsWith,
102
    sub2_field: Option<(String,)>,
103
    #[deftly(netdoc(single_arg))]
104
    arg_needed: String,
105
    #[deftly(netdoc(single_arg))]
106
    arg_optional: Option<String>,
107
    #[deftly(netdoc(single_arg))]
108
    arg_several: Vec<String>,
109
    #[deftly(netdoc(single_arg, default))]
110
    arg_defaulted: i32,
111
    #[deftly(netdoc(with = "needs_with_parse"))]
112
    with_needed: NeedsWith,
113
    #[deftly(netdoc(with = "needs_with_parse"))]
114
    with_optional: Option<NeedsWith>,
115
    #[deftly(netdoc(with = "needs_with_parse"))]
116
    with_several: Vec<NeedsWith>,
117
    #[deftly(netdoc(subdoc))]
118
    subsub: SubSub,
119
}
120
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
121
#[derive_deftly(NetdocEncodable, NetdocParseable)]
122
struct Sub3 {
123
    sub3_intro: (),
124
    sub3_field: Option<(String,)>,
125
}
126
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
127
#[derive_deftly(NetdocEncodable, NetdocParseable)]
128
struct Sub4 {
129
    sub4_intro: (),
130
    sub4_field: Option<(String,)>,
131
}
132
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
133
#[derive_deftly(NetdocEncodable, NetdocParseable)]
134
struct SubSub {
135
    #[deftly(netdoc(single_arg))]
136
    subsub_intro: String,
137
    subsub_field: Option<(String,)>,
138
}
139

            
140
#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
141
struct NeedsWith;
142

            
143
impl NeedsWith {
144
112
    fn parse_expecting(exp: &str, args: &mut ArgumentStream<'_>) -> Result<NeedsWith, P2AE> {
145
112
        let got = args.next().ok_or(P2AE::Missing)?;
146
110
        (got == exp).then_some(NeedsWith).ok_or(P2AE::Invalid)
147
112
    }
148
}
149

            
150
mod needs_with_parse {
151
    use super::*;
152
82
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
153
82
        NeedsWith::parse_expecting("normal", item.args_mut())
154
82
            .map_err(item.args().error_handler("in needs with"))
155
82
    }
156
    #[allow(clippy::unnecessary_wraps)]
157
24
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
158
24
        out.arg(&"normal");
159
24
        Ok(())
160
24
    }
161
}
162
mod needs_with_intro {
163
    use super::*;
164
18
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
165
18
        NeedsWith::parse_expecting("intro", item.args_mut())
166
18
            .map_err(item.args().error_handler("in needs with"))
167
18
    }
168
    #[allow(clippy::unnecessary_wraps)]
169
4
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
170
4
        out.arg(&"intro");
171
4
        Ok(())
172
4
    }
173
}
174
mod needs_with_arg {
175
    use super::*;
176
12
    pub(super) fn from_args(args: &mut ArgumentStream) -> Result<NeedsWith, P2AE> {
177
12
        NeedsWith::parse_expecting("arg", args)
178
12
    }
179
    #[allow(clippy::unnecessary_wraps)]
180
4
    pub(super) fn write_arg_onto(_self: &NeedsWith, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
181
4
        out.args_raw_string(&"arg");
182
4
        Ok(())
183
4
    }
184
6
    pub(super) fn from_args_rest(s: &str) -> Result<NeedsWith, ()> {
185
6
        (s == "rest of line").then_some(NeedsWith).ok_or(())
186
6
    }
187
    #[allow(clippy::unnecessary_wraps)]
188
2
    pub(super) fn fmt_args_rest(_self: &NeedsWith, f: &mut fmt::Formatter) -> fmt::Result {
189
2
        write!(f, "rest of line")
190
2
    }
191
}
192

            
193
/// Test parsing and encoding of a single-document file
194
///
195
/// `doc_spec` is the document to parse.
196
/// `exp` is what it should parse as.
197
///
198
/// `doc_spec` can have magic instructions at end of each line.
199
/// These allow the re-encoding to be not quite identical to the input document.
200
///
201
///  * **`@ re-encoded:`:
202
///    This line is re-encoded differently.  The *next* source line is the encoding.
203
///
204
///  * **`@ not re-encoded`:
205
///    This line is omitted from the re-encoding.
206
///
207
///  * **`@ re-encoded later N`:
208
///    This line is reordered, to later in the re-encoding, by `N` lines.
209
///
210
///  * **`@ re-encoded only`:
211
///    This line eppears only in the re-encoding.
212
///    Prefer `re-encoded later` or `re-encoded:` if possible as they're clearer.
213
14
fn t_ok<D>(doc_spec: &str, exp: &D) -> TestResult<()>
214
14
where
215
14
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
216
{
217
14
    t_ok_multi::<D>(&[], doc_spec, slice::from_ref(exp))
218
14
}
219

            
220
/// Test parsing and encoding of a multi-document file
221
///
222
/// The de/re-encoding syntax is as above.
223
//
224
// It would perhaps be better if `doc_boundaries` were obtained from magic instructions,
225
// but there's only one test case with a fragile hardcoded byte offset ATM.
226
16
fn t_ok_multi<D>(doc_boundaries: &[usize], doc_spec: &str, exp: &[D]) -> TestResult<()>
227
16
where
228
16
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
229
{
230
16
    eprintln!("#####");
231
16
    eprint!("====== doc_spec ======\n{doc_spec}");
232
16
    eprintln!("====== exp ======\n{exp:#?}");
233

            
234
16
    let mut lines = doc_spec.split_inclusive('\n');
235
16
    let mut doc = String::new();
236
16
    let mut enc = String::new();
237

            
238
    // indices are line numbers but starting at 0
239
16
    let mut moved = Vec::<String>::new();
240
216
    let process_moved = |enc: &mut String, moved: &mut Vec<String>| {
241
216
        if moved.is_empty() {
242
126
            return;
243
90
        }
244
        loop {
245
112
            let lno = enc.lines().count();
246
112
            let Some(m) = moved.get_mut(lno) else {
247
36
                eprintln!("PN {lno:2} nothing");
248
36
                break;
249
            };
250
76
            if m.is_empty() {
251
54
                eprintln!("PN {lno:2} empty");
252
54
                break;
253
22
            }
254
22
            eprintln!("PN {lno:2} adding {m:?}");
255
22
            *enc += &mem::take(m);
256
        }
257
216
    };
258

            
259
216
    while let Some(l) = lines.next() {
260
200
        if let Some((l, insn)) = l.split_once('@') {
261
56
            eprintln!("LL    insn  {l:?}");
262
56
            let insn = insn.trim();
263
56
            let l = &format!("{}\n", l.trim_end());
264
56
            let insn = insn.trim_end();
265
56
            if insn == "re-encoded:" {
266
6
                doc += l;
267
6
                enc += lines.next().expect(r#""re-encoded:" needs re-encoded"#);
268
50
            } else if insn == "not re-encoded" {
269
10
                doc += l;
270
40
            } else if insn == "re-encoded only" {
271
12
                enc += l;
272
28
            } else if let Some(later) = insn.strip_prefix("re-encoded later ") {
273
28
                doc += l;
274
28
                let later: usize = later.parse().expect(later);
275
28
                let lno = later + enc.lines().count();
276
                loop {
277
120
                    if let Some(m) = moved.get_mut(lno) {
278
28
                        *m += l;
279
28
                        break;
280
92
                    }
281
92
                    moved.push("".into());
282
                }
283
            } else {
284
                panic!("unknown insn {insn:?} in {doc_spec:?}");
285
            }
286
144
        } else {
287
144
            eprintln!("LL    line  {l:?}");
288
144
            doc += l;
289
144
            enc += l;
290
144
        }
291
200
        process_moved(&mut enc, &mut moved);
292
    }
293
16
    process_moved(&mut enc, &mut moved);
294
92
    for (i, l) in moved.iter().enumerate() {
295
92
        assert_eq!(l, "", "line too late! {}: {l:?}", i + 1);
296
    }
297

            
298
16
    eprint!("====== doc ======\n{doc}");
299
16
    eprint!("====== enc exp ======\n{enc}");
300
16
    eprintln!("======");
301

            
302
16
    let input = ParseInput::new(&doc, "<literal>");
303

            
304
16
    if exp.len() == 1 {
305
14
        let got = parse_netdoc::<D>(&input).context(doc.clone())?;
306
14
        assert_eq!(got, exp[0], "parse 1 mismatch");
307
2
    }
308

            
309
16
    let got = parse_netdoc_multiple::<D>(&input)?;
310
16
    assert_eq!(got, exp, "parse_multiple mismatch");
311

            
312
16
    let got_with_offsets = parse_netdoc_multiple_with_offsets::<D>(&input)?;
313
18
    for (i, (got, start, end)) in got_with_offsets.iter().enumerate() {
314
18
        assert_eq!(got, &exp[i], "parse_multiple_with_offsets mismatch");
315
18
        assert_eq!(*start, if i == 0 { 0 } else { doc_boundaries[i - 1] });
316
18
        assert_eq!(*end, doc_boundaries.get(i).copied().unwrap_or(doc.len()));
317
    }
318

            
319
16
    let reenc = {
320
16
        let mut encoder = NetdocEncoder::default();
321
34
        for d in exp {
322
18
            d.encode_unsigned(&mut encoder)?;
323
        }
324
16
        encoder.finish()?
325
    };
326

            
327
16
    eprintln!("====== enc got ======\n{reenc}====== end ======");
328

            
329
16
    assert_eq!(
330
16
        &enc,
331
16
        &reenc,
332
        "re-encode mismatch:\n{}",
333
        Itertools::zip_longest(
334
            chain!(["EXPECTED"], enc.lines()),
335
            chain!(["GOT"], reenc.lines()),
336
        )
337
        .enumerate()
338
        .map(|(i, eob)| {
339
            let lno = i + 1;
340
            let [l, r] = [eob.clone().left(), eob.right()];
341
            let yn = if l == r { "  " } else { "!=" };
342
            let [l, r] = [l, r].map(|s| s.unwrap_or_default());
343
            format!(" {lno:2} {l:30} {yn} {r}\n")
344
        })
345
        .collect::<String>(),
346
    );
347

            
348
16
    Ok(())
349
16
}
350

            
351
#[allow(clippy::unnecessary_wraps)] // Result for consistency
352
38
fn t_err_raw<D>(
353
38
    exp_lno: usize,
354
38
    exp_col: Option<usize>,
355
38
    exp_err: &str,
356
38
    doc: &str,
357
38
) -> TestResult<ParseError>
358
38
where
359
38
    D: NetdocParseable + Debug,
360
{
361
38
    let input = ParseInput::new(doc, "<massaged>");
362
38
    let got = parse_netdoc::<D>(&input).expect_err("unexpectedly parsed ok");
363
38
    let got_err = got.problem.to_string();
364
38
    assert_eq!(
365
38
        (got.lno, got.column),
366
38
        (exp_lno, exp_col),
367
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
368
        got_err
369
    );
370
38
    assert_eq!(
371
        got_err, exp_err,
372
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
373
        got_err
374
    );
375
38
    Ok(got)
376
38
}
377

            
378
/// Test an error case with embedded error message
379
///
380
/// `case` should be the input document, but exactly one line should
381
/// contain `" # "`, with the expected error message as a "comment".
382
///
383
/// Iff the expected message is supposed to have a column number,
384
/// the comment part should end with ` @<column>`.
385
///
386
/// `t_err` will check that that error is reported, at that line.
387
36
fn t_err<D>(mut case: &str) -> TestResult<ParseError>
388
36
where
389
36
    D: NetdocParseable + Debug,
390
{
391
36
    let mut exp = None;
392
36
    let mut doc = String::new();
393
36
    let mut lno = 0;
394
192
    while let Some((l, r)) = case.split_once('\n') {
395
156
        lno += 1;
396
156
        case = r;
397
156
        if let Some((l, r)) = l.split_once(" # ") {
398
36
            assert!(exp.is_none());
399
36
            exp = Some((lno, r.trim()));
400
36
            let l = l.trim_end();
401
36
            doc += l;
402
120
        } else {
403
120
            doc += l;
404
120
        }
405
156
        doc += "\n";
406
    }
407
36
    if !case.is_empty() {
408
        panic!("missing final newline");
409
36
    }
410
36
    let (exp_lno, exp_err) = exp.expect("missing # error indication in test case");
411
36
    let (exp_err, exp_col) = if let Some((l, r)) = exp_err.rsplit_once(" @") {
412
4
        (l, Some(r.parse().unwrap()))
413
    } else {
414
32
        (exp_err, None)
415
    };
416
36
    println!("==== 8<- ====\n{doc}==== ->8 ====");
417
36
    t_err_raw::<D>(exp_lno, exp_col, exp_err, &doc)
418
36
}
419

            
420
/// Test an error case with embedded error message
421
///
422
/// `case` should be the input document, but exactly one line should
423
/// contain `" # "`, with the expected error message as a "comment".
424
///
425
/// Iff the expected message is supposed to have a column number,
426
/// the comment part should end with ` @<column>`.
427
///
428
/// `t_err` will check that that error is reported, at that column.
429
4
fn t_err_chk_msg<D>(case: &str, msg: &str) -> TestResult
430
4
where
431
4
    D: NetdocParseable + Debug,
432
{
433
4
    let err = t_err::<D>(case)?;
434
4
    assert_eq!(err.report().to_string(), msg);
435
4
    Ok(())
436
4
}
437

            
438
#[test]
439
2
fn various_docs() -> TestResult<()> {
440
41
    let val = |s: &str| (s.to_owned(),);
441
23
    let sval = |s: &str| Some(val(s));
442

            
443
2
    let sub1_minimal = Sub1 {
444
2
        flatten: Flat1 {
445
2
            flat_needed: val("FN"),
446
2
            flat_arg_needed: "FAN".into(),
447
2
            ..default()
448
2
        },
449
2
        ..default()
450
2
    };
451
2
    let subsub_minimal = SubSub {
452
2
        subsub_intro: "SSI".into(),
453
2
        ..default()
454
2
    };
455
2
    let sub2_minimal = Sub2 {
456
2
        arg_needed: "AN".into(),
457
2
        subsub: subsub_minimal.clone(),
458
2
        ..default()
459
2
    };
460

            
461
2
    t_ok(
462
2
        r#"top-intro
463
2
needed N
464
2
defaulted 0                             @ re-encoded only
465
2
sub1-intro
466
2
flat-needed FN
467
2
flat-arg-needed FAN
468
2
flat-arg-defaulted 0                    @ re-encoded only
469
2
flat-with-needed normal
470
2
sub4-intro                              @ re-encoded only
471
2
"#,
472
2
        &Top {
473
2
            needed: val("N"),
474
2
            sub1: sub1_minimal.clone(),
475
2
            ..default()
476
2
        },
477
    )?;
478

            
479
2
    t_ok(
480
2
        r#"top-intro
481
2
needed N
482
2
defaulted 0                             @ re-encoded only
483
2
sub1-intro
484
2
flat-needed FN
485
2
flat-arg-needed FAN
486
2
flat-arg-defaulted 0                    @ re-encoded only
487
2
flat-with-needed normal
488
2
sub2-intro intro
489
2
with-needed normal                      @ re-encoded later 2
490
2
arg-needed AN
491
2
arg-defaulted 0                         @ re-encoded only
492
2
subsub-intro SSI
493
2
sub3-intro
494
2
sub3-intro
495
2
sub4-intro
496
2
"#,
497
2
        &Top {
498
2
            needed: val("N"),
499
2
            sub1: sub1_minimal.clone(),
500
2
            sub2: Some(sub2_minimal.clone()),
501
2
            sub3: vec![default(); 2],
502
2
            ..default()
503
2
        },
504
    )?;
505

            
506
2
    t_ok(
507
2
        r#"top-intro
508
2
needed N
509
2
optional O
510
2
several 1
511
2
not-present oh yes it is                @ not re-encoded
512
2
not-present but it is ignored           @ not re-encoded
513
2
several 2
514
2
defaulted -1
515
2
renamed R
516
2
sub1-intro
517
2
flat-several FS1                        @ re-encoded later 3
518
2
flat-needed FN                          @ re-encoded later 1
519
2
flat-with-needed normal                 @ re-encoded later 11
520
2
flat-inner-optional nested              @ re-encoded later 15
521
2
sub1-field A
522
2
flat-with-several normal                @ re-encoded later 11
523
2
flat-with-several normal                @ re-encoded later 11
524
2
flat-optional FO
525
2
flat-arg-needed FAN                     @ re-encoded later 2
526
2
flat-with-optional normal               @ re-encoded later 8
527
2
flat-several FS2
528
2
flat-defaulted FD
529
2
flat-arg-optional FAO
530
2
flat-arg-several FAS1 ignored           @ re-encoded:
531
2
flat-arg-several FAS1
532
2
flat-arg-several FAS2
533
2
flat-arg-defaulted 31
534
2
sub2-intro intro
535
2
with-several normal                     @ re-encoded later 8
536
2
with-several normal                     @ re-encoded later 8
537
2
with-several normal                     @ re-encoded later 8
538
2
sub2-field B
539
2
arg-needed AN
540
2
arg-optional AO
541
2
with-optional normal                    @ re-encoded later 4
542
2
arg-defaulted 4                         @ re-encoded later 2
543
2
arg-several A1
544
2
arg-several A2
545
2
with-needed normal
546
2
subsub-intro SSI
547
2
subsub-field BS
548
2
sub3-intro
549
2
sub3-field C1
550
2
sub3-intro
551
2
sub3-field C2
552
2
sub4-intro
553
2
sub4-field D
554
2
"#,
555
        &Top {
556
2
            needed: val("N"),
557
2
            optional: sval("O"),
558
2
            several: ["1", "2"].map(val).into(),
559
2
            defaulted: (-1,),
560
2
            t4_renamed: sval("R"),
561
2
            sub1: Sub1 {
562
2
                sub1_field: sval("A"),
563
2
                flatten: Flat1 {
564
2
                    flat_needed: val("FN"),
565
2
                    flat_optional: sval("FO"),
566
2
                    flat_several: ["FS1", "FS2"].map(val).into(),
567
2
                    flat_defaulted: sval("FD"),
568
2
                    flat_arg_needed: "FAN".into(),
569
2
                    flat_arg_several: ["FAS1", "FAS2"].map(Into::into).into(),
570
2
                    flat_arg_optional: Some("FAO".into()),
571
2
                    flat_arg_defaulted: 31,
572
2
                    flat_with_optional: Some(NeedsWith),
573
2
                    flat_with_several: vec![NeedsWith; 2],
574
2
                    flat_flat: FlatInner {
575
2
                        flat_inner_optional: sval("nested"),
576
2
                    },
577
2
                    ..Flat1::default()
578
2
                },
579
2
                ..default()
580
2
            },
581
2
            sub2: Some(Sub2 {
582
2
                sub2_field: sval("B"),
583
2
                arg_optional: Some("AO".into()),
584
2
                arg_defaulted: 4,
585
2
                arg_several: ["A1", "A2"].map(Into::into).into(),
586
2
                with_optional: Some(NeedsWith),
587
2
                with_several: vec![NeedsWith; 3],
588
2
                subsub: SubSub {
589
2
                    subsub_field: sval("BS"),
590
2
                    ..subsub_minimal.clone()
591
2
                },
592
2
                ..sub2_minimal.clone()
593
2
            }),
594
2
            sub3: ["C1", "C2"]
595
2
                .map(|s| Sub3 {
596
4
                    sub3_field: sval(s),
597
4
                    ..default()
598
4
                })
599
2
                .into(),
600
2
            sub4: Sub4 {
601
2
                sub4_field: sval("D"),
602
2
                ..default()
603
2
            },
604
2
            ..default()
605
        },
606
    )?;
607

            
608
2
    t_err_raw::<Top>(0, None, "empty document", r#""#)?;
609

            
610
2
    let wrong_document = r#"wrong-keyword # wrong document type
611
2
"#;
612
2
    t_err_chk_msg::<Top>(
613
2
        wrong_document,
614
2
        "error: failed to parse network document, type top-intro: <massaged>:1: wrong document type",
615
    )?;
616

            
617
2
    t_err::<Top>(
618
2
        r#"top-intro
619
2
sub4-intro # missing item needed
620
2
"#,
621
    )?;
622

            
623
2
    t_err::<Top>(
624
2
        r#"top-intro
625
2
sub1-intro
626
2
flat-arg-needed arg
627
2
flat-with-needed normal
628
2
sub4-intro # missing item flat-needed
629
2
"#,
630
    )?;
631

            
632
2
    t_err::<Top>(
633
2
        r#"top-intro
634
2
sub1-intro
635
2
flat-needed flat
636
2
flat-with-needed normal
637
2
sub4-intro # missing item flat-arg-needed
638
2
"#,
639
    )?;
640

            
641
2
    t_err::<Top>(
642
2
        r#"top-intro
643
2
sub1-intro
644
2
flat-arg-needed FAN
645
2
flat-with-needed normal
646
2
flat-needed FN
647
2
sub4-intro # missing item needed
648
2
"#,
649
    )?;
650

            
651
2
    t_err::<Top>(
652
2
        r#"top-intro
653
2
needed N
654
2
sub3-intro
655
2
sub4-intro # missing item sub1-intro
656
2
"#,
657
    )?;
658

            
659
2
    t_err::<Top>(
660
2
        r#"top-intro
661
2
needed N
662
2
sub1-intro
663
2
flat-needed FN1
664
2
flat-arg-needed FAN
665
2
flat-with-needed normal
666
2
sub1-intro # item repeated when not allowed
667
2
flat-needed FN2
668
2
"#,
669
    )?;
670

            
671
2
    t_err::<Top>(
672
2
        r#"top-intro
673
2
needed N
674
2
sub2-intro # missing argument in needs with
675
2
"#,
676
    )?;
677

            
678
2
    let wrong_value = r#"top-intro
679
2
needed N
680
2
sub2-intro wrong-value # invalid value for argument in needs with @12
681
2
"#;
682
2
    t_err_chk_msg::<Top>(
683
2
        wrong_value,
684
2
        "error: failed to parse network document, type top-intro: <massaged>:3.12: invalid value for argument in needs with",
685
    )?;
686

            
687
2
    t_err::<Top>(
688
2
        r#"top-intro
689
2
sub1-intro
690
2
flat-needed FN
691
2
flat-arg-needed arg
692
2
sub4-intro # missing item flat-with-needed
693
2
"#,
694
    )?;
695

            
696
2
    t_err::<Top>(
697
2
        r#"top-intro
698
2
sub1-intro
699
2
flat-needed FN
700
2
flat-arg-needed arg
701
2
flat-with-needed normal
702
2
sub2-intro intro
703
2
arg-needed AN
704
2
flat-arg-needed arg
705
2
sub3-intro # missing item with-needed
706
2
"#,
707
    )?;
708

            
709
2
    Ok(())
710
2
}
711

            
712
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
713
#[derive_deftly(NetdocEncodable, NetdocParseable)]
714
struct TopMinimal {
715
    test_item0: TestItem0,
716
    test_item: Option<TestItem>,
717
    test_item_rest: Option<TestItemRest>,
718
    test_item_rest_with: Option<TestItemRestWith>,
719
    test_item_object_not_present: Option<TestItemObjectNotPresent>,
720
    test_item_object_ignored: Option<TestItemObjectIgnored>,
721
    #[deftly(netdoc(skip))]
722
    __test_skip: (),
723
}
724

            
725
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
726
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
727
#[deftly(netdoc(no_extra_args))]
728
struct TestItem0 {
729
    #[deftly(netdoc(object(label = "UTF-8 STRING"), with = "string_data_object"))]
730
    object: Option<String>,
731
}
732

            
733
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
734
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
735
struct TestItem {
736
    needed: String,
737
    #[deftly(netdoc(with = "needs_with_arg"))]
738
    optional: Option<NeedsWith>,
739
    rest: Vec<String>,
740
    #[deftly(netdoc(object))]
741
    object: TestObject,
742
}
743

            
744
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
745
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
746
struct TestItemRest {
747
    optional: Option<String>,
748
    #[deftly(netdoc(rest))]
749
    rest: String,
750
}
751

            
752
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
753
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
754
struct TestItemRestWith {
755
    #[deftly(netdoc(rest, with = "needs_with_arg"))]
756
    rest: NeedsWith,
757
}
758

            
759
#[derive(Debug, Default, Clone, Eq, PartialEq)]
760
struct TestObject(String);
761

            
762
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
763
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
764
struct TestItemObjectNotPresent {
765
    #[deftly(netdoc(object))]
766
    object: NotPresent,
767
}
768

            
769
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
770
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
771
struct TestItemObjectIgnored {
772
    #[deftly(netdoc(object))]
773
    object: Ignored,
774
}
775

            
776
/// Conversion module for `String` as Object with [`ItemValueParseable`]
777
mod string_data_object {
778
    /// Parse the data
779
8
    pub(super) fn try_from(data: Vec<u8>) -> Result<String, std::string::FromUtf8Error> {
780
8
        String::from_utf8(data)
781
8
    }
782

            
783
    /// Encode the data
784
    #[allow(clippy::unnecessary_wraps)] // signature must match the derive's expectation
785
2
    pub(super) fn write_object_onto<B>(self_: &String, b: &mut B) -> tor_bytes::EncodeResult<()>
786
2
    where
787
2
        B: tor_bytes::Writer + ?Sized,
788
    {
789
2
        b.write_all(self_.as_bytes());
790
2
        Ok(())
791
2
    }
792
}
793

            
794
impl ItemObjectParseable for TestObject {
795
18
    fn check_label(label: &str) -> Result<(), P2EP> {
796
18
        if label != "TEST OBJECT" {
797
            return Err(P2EP::ObjectIncorrectLabel);
798
18
        }
799
18
        Ok(())
800
18
    }
801
18
    fn from_bytes(data: &[u8]) -> Result<Self, P2EP> {
802
        Ok(TestObject(
803
18
            String::from_utf8(data.to_owned()).map_err(|_| P2EP::ObjectInvalidData)?,
804
        ))
805
18
    }
806
}
807
impl ItemObjectEncodable for TestObject {
808
6
    fn label(&self) -> &'static str {
809
6
        "TEST OBJECT"
810
6
    }
811
6
    fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug> {
812
6
        b.extend(self.0.as_bytes());
813
6
        Ok(())
814
6
    }
815
}
816

            
817
#[test]
818
2
fn various_items() -> TestResult<()> {
819
2
    let test_item_minimal = TestItem {
820
2
        needed: "N".into(),
821
2
        object: TestObject("hello".into()),
822
2
        ..default()
823
2
    };
824

            
825
2
    t_ok(
826
2
        r#"test-item0
827
2
"#,
828
2
        &TopMinimal { ..default() },
829
    )?;
830

            
831
2
    t_ok(
832
2
        r#"test-item0
833
2
test-item N
834
2
-----BEGIN TEST OBJECT-----
835
2
aGVsbG8=
836
2
-----END TEST OBJECT-----
837
2
"#,
838
2
        &TopMinimal {
839
2
            test_item: Some(test_item_minimal.clone()),
840
2
            ..default()
841
2
        },
842
    )?;
843

            
844
2
    t_ok(
845
2
        r#"test-item0
846
2
test-item N arg
847
2
-----BEGIN TEST OBJECT-----
848
2
aGVsbG8=
849
2
-----END TEST OBJECT-----
850
2
"#,
851
2
        &TopMinimal {
852
2
            test_item: Some(TestItem {
853
2
                optional: Some(NeedsWith),
854
2
                ..test_item_minimal.clone()
855
2
            }),
856
2
            ..default()
857
2
        },
858
    )?;
859

            
860
2
    t_ok(
861
2
        r#"test-item0
862
2
-----BEGIN UTF-8 STRING-----
863
2
aGVsbG8=
864
2
-----END UTF-8 STRING-----
865
2
test-item N arg R1 R2
866
2
-----BEGIN TEST OBJECT-----
867
2
aGVsbG8=
868
2
-----END TEST OBJECT-----
869
2
test-item-rest O  and  the rest                 @ re-encoded:
870
2
test-item-rest O and  the rest
871
2
test-item-rest-with   rest of line              @ re-encoded:
872
2
test-item-rest-with rest of line
873
2
test-item-object-not-present
874
2
test-item-object-ignored
875
2
-----BEGIN TEST OBJECT-----                     @ not re-encoded
876
2
aGVsbG8=         @ not re-encoded
877
2
-----END TEST OBJECT-----                       @ not re-encoded
878
2
"#,
879
2
        &TopMinimal {
880
2
            test_item0: TestItem0 {
881
2
                object: Some("hello".into()),
882
2
            },
883
2
            test_item: Some(TestItem {
884
2
                optional: Some(NeedsWith),
885
2
                rest: ["R1", "R2"].map(Into::into).into(),
886
2
                ..test_item_minimal.clone()
887
2
            }),
888
2
            test_item_rest: Some(TestItemRest {
889
2
                optional: Some("O".into()),
890
2
                rest: "and  the rest".into(),
891
2
            }),
892
2
            test_item_rest_with: Some(TestItemRestWith { rest: NeedsWith }),
893
2
            test_item_object_not_present: Some(TestItemObjectNotPresent { object: NotPresent }),
894
2
            test_item_object_ignored: Some(TestItemObjectIgnored { object: Ignored }),
895
2
            __test_skip: (),
896
2
        },
897
    )?;
898

            
899
2
    t_ok_multi(
900
2
        &[11],
901
2
        r#"test-item0
902
2
test-item0
903
2
test-item-rest optional resty rest
904
2
"#,
905
2
        &[
906
2
            TopMinimal::default(),
907
2
            TopMinimal {
908
2
                test_item_rest: Some(TestItemRest {
909
2
                    optional: Some("optional".into()),
910
2
                    rest: "resty rest".into(),
911
2
                }),
912
2
                ..default()
913
2
            },
914
2
        ],
915
    )?;
916

            
917
2
    t_err::<TopMinimal>(
918
2
        r#"test-item0 wrong # too many arguments @12
919
2
"#,
920
    )?;
921
2
    t_err::<TopMinimal>(
922
2
        r#"test-item0 # base64-encoded Object label is not as expected
923
2
-----BEGIN WRONG LABEL-----
924
2
aGVsbG8=
925
2
-----END WRONG LABEL-----
926
2
"#,
927
    )?;
928
2
    t_err::<TopMinimal>(
929
2
        r#"test-item0 # base64-encoded Object END label does not match BEGIN
930
2
-----BEGIN UTF-8 STRING-----
931
2
aGVsbG8=
932
2
-----END WRONG LABEL-----
933
2
"#,
934
    )?;
935
2
    t_err::<TopMinimal>(
936
2
        r#"test-item0
937
2
test-item-object-not-present # base64-encoded Object found where none expected
938
2
-----BEGIN TEST OBJECT-----
939
2
aGVsbG8=
940
2
-----END TEST OBJECT-----
941
2
"#,
942
    )?;
943
2
    t_err::<TopMinimal>(
944
2
        r#"test-item0 # base64-encoded Object has incorrectly formatted delimiter lines
945
2
-----BEGIN UTF-8 STRING-----
946
2
aGVsbG8=
947
2
-----END UTF-8 STRING
948
2
"#,
949
    )?;
950
2
    t_err::<TopMinimal>(
951
2
        r#"test-item0 # base64-encoded Object contains invalid base64
952
2
-----BEGIN UTF-8 STRING-----
953
2
bad b64 !
954
2
-----END UTF-8 STRING-----
955
2

            
956
2
"#,
957
    )?;
958
2
    t_err::<TopMinimal>(
959
2
        r#"test-item0 # base64-encoded Object contains invalid data
960
2
-----BEGIN UTF-8 STRING-----
961
2
hU6Qo2fW7+9PXkcrEyiB62ZDne/gwKPHXBo8lMeV8JCOfVBF5vT4BtKRLP+Jw66x
962
2
-----END UTF-8 STRING-----
963
2
"#,
964
    )?;
965

            
966
2
    Ok(())
967
2
}