1use std::fmt::Display;
2
3use winnow::ascii::{line_ending, multispace0, multispace1, space0, space1};
4use winnow::combinator::{alt, cut_err, delimited, opt, preceded, repeat};
5use winnow::error::{ContextError, ParseError, StrContext, StrContextValue};
6use winnow::token::{rest, take_till, take_until, take_while};
7use winnow::{LocatingSlice, ModalResult, Parser};
8
9type Input<'a> = LocatingSlice<&'a str>;
10
11fn parse_valid_string<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
17 let spaces_and_symbols = "\t /-_@.,%#'";
18 take_while(1.., move |c: char| {
19 c.is_alphanumeric() || spaces_and_symbols.contains(c)
20 })
21 .parse_next(input)
22}
23
24fn parse_comment<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
30 delimited(
31 "/*",
32 cut_err(take_until(0.., "*/"))
33 .context(StrContext::Expected(StrContextValue::StringLiteral("*/")))
34 .map(|v: &str| v.trim()),
35 ("*/", space0),
36 )
37 .parse_next(input)
38}
39
40fn parse_curly<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
47 delimited(
48 "{",
49 parse_valid_string.map(|v| v.trim()),
50 cut_err("}").context(StrContext::Expected(StrContextValue::CharLiteral('}'))),
51 )
53 .parse_next(input)
54}
55
56fn parse_quantity<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
67 let spaces_and_symbols = ".,/_";
68
69 cut_err(
70 take_while(1.., move |c: char| {
71 c.is_numeric() || spaces_and_symbols.contains(c)
72 })
73 .verify(|s: &str| {
74 let has_repeated_symbols = s
76 .as_bytes()
77 .windows(2)
78 .any(|v| v[0] == v[1] && spaces_and_symbols.contains(char::from(v[0])));
79 let last_char = &s[s.len() - 1..];
80 !spaces_and_symbols.contains(last_char) && !has_repeated_symbols
81 }),
82 )
83 .context(StrContext::Expected(StrContextValue::Description(
84 "a quantity value, like 3, 1.2, 1/2 or 1_000",
85 )))
86 .parse_next(input)
87}
88
89fn parse_unit<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
91 parse_valid_string.parse_next(input)
92}
93
94fn parse_ingredient_amount<'a>(
96 input: &mut Input<'a>,
97) -> ModalResult<(Option<&'a str>, Option<&'a str>)> {
98 delimited(
99 ("(", space0),
100 (
101 opt(parse_quantity),
102 opt(preceded(space0, parse_unit.map(|v| v.trim()))),
103 ),
104 cut_err(")").context(StrContext::Expected(StrContextValue::CharLiteral(')'))), )
106 .parse_next(input)
108}
109
110fn parse_ingredient<'a>(
118 input: &mut Input<'a>,
119) -> ModalResult<(&'a str, Option<(Option<&'a str>, Option<&'a str>)>)> {
120 (parse_curly, opt(parse_ingredient_amount)).parse_next(input)
121}
122
123fn parse_material<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
131 preceded("&", parse_curly).parse_next(input)
132}
133
134fn parse_timer<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
141 preceded("t", parse_curly).parse_next(input)
142}
143
144fn parse_recipe_ref<'a>(
151 input: &mut Input<'a>,
152) -> ModalResult<(&'a str, Option<(Option<&'a str>, Option<&'a str>)>)> {
153 preceded("@", (parse_curly, opt(parse_ingredient_amount))).parse_next(input)
154}
155
156fn parse_word<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
158 take_till(1.., (' ', '\t', '\r', '\n', '\'', '`')).parse_next(input)
159}
160
161fn parse_metadata<'a>(input: &mut Input<'a>) -> ModalResult<(&'a str, &'a str)> {
162 preceded(
163 (">>", space0),
164 (
165 take_while(1.., |c| c != ':'),
166 preceded((":", space0), take_until(0.., "\n")),
167 ),
168 )
169 .parse_next(input)
170}
171
172fn parse_backstory<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
179 preceded(
180 delimited(
181 preceded(line_ending, multispace0),
182 "---",
183 preceded(line_ending, multispace0),
184 ),
185 rest,
186 )
187 .parse_next(input)
188}
189
190fn parse_special_symbols<'a>(input: &mut Input<'a>) -> ModalResult<&'a str> {
202 alt(("(", "'", "`")).parse_next(input)
203}
204#[derive(Debug, Clone, Eq, PartialEq, Hash)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize))]
211#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
212#[cfg_attr(feature = "serde", serde(tag = "token", content = "content"))]
214#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
215#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
216pub enum Token<'a> {
217 Metadata {
224 key: &'a str,
225 value: &'a str,
226 },
227
228 Ingredient {
236 name: &'a str,
237 quantity: Option<&'a str>,
238 unit: Option<&'a str>,
239 },
240
241 RecipeRef {
249 name: &'a str,
250 quantity: Option<&'a str>,
251 unit: Option<&'a str>,
252 },
253
254 Timer(&'a str),
262
263 Material(&'a str),
271
272 Word(&'a str),
273 Space(&'a str),
274 Comment(&'a str),
275
276 Backstory(&'a str),
286}
287
288impl Display for Token<'_> {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 match self {
291 Token::Ingredient {
292 name,
293 quantity: _,
294 unit: _,
295 } => write!(f, "{}", name),
296 Token::RecipeRef {
297 name,
298 quantity: _,
299 unit: _,
300 } => write!(f, "\"{}\"", name),
301 Token::Backstory(v)
302 | Token::Timer(v)
303 | Token::Material(v)
304 | Token::Word(v)
305 | Token::Space(v) => {
306 write!(f, "{}", v)
307 }
308 Token::Metadata { key: _, value: _ } => Ok(()),
309 Token::Comment(_) => Ok(()),
310 }
311 }
312}
313
314pub fn recipe_value<'a>(input: &mut Input<'a>) -> ModalResult<Token<'a>> {
315 alt((
316 parse_metadata.map(|(key, value)| Token::Metadata { key, value }),
317 parse_material.map(|m| Token::Material(m)),
318 parse_timer.map(|t| Token::Timer(t)),
319 parse_ingredient.map(|(name, amount)| {
322 let mut quantity = None;
323 let mut unit = None;
324 if let Some((_quantity, _unit)) = amount {
325 quantity = _quantity;
326 unit = _unit;
327 };
328
329 Token::Ingredient {
330 name,
331 quantity,
332 unit,
333 }
334 }),
335 parse_recipe_ref.map(|(name, amount)| {
336 let mut quantity = None;
337 let mut unit = None;
338 if let Some((_quantity, _unit)) = amount {
339 quantity = _quantity;
340 unit = _unit;
341 };
342
343 Token::RecipeRef {
344 name,
345 quantity,
346 unit,
347 }
348 }),
349 parse_backstory.map(|v| Token::Backstory(v)),
350 parse_comment.map(|v| Token::Comment(v)),
351 parse_special_symbols.map(|v| Token::Word(v)),
352 parse_word.map(|v| Token::Word(v)),
353 space1.map(|v| Token::Space(v)),
354 multispace1.map(|v| Token::Space(v)),
355 ))
356 .parse_next(input)
357}
358
359pub fn recipe<'a>(input: &mut Input<'a>) -> ModalResult<Vec<Token<'a>>> {
360 repeat(0.., recipe_value).parse_next(input)
361}
362
363pub fn parse(input: &str) -> Result<Vec<Token<'_>>, ParseError<LocatingSlice<&str>, ContextError>> {
376 let input = LocatingSlice::new(input);
377 recipe.parse(input)
378}
379
380#[cfg(test)]
381mod test {
382 use super::*;
383 use rstest::*;
384
385 #[rstest]
386 #[case("salt", "salt")]
387 #[case("sweet potato", "sweet potato")]
388 #[case("ToMaToeS", "ToMaToeS")]
389 #[case("1/2 lemon", "1/2 lemon")]
390 #[case("my-best-sauce", "my-best-sauce")]
391 #[case("1.2", "1.2")]
392 #[case("1,2", "1,2")]
393 #[case("1_200", "1_200")]
394 #[case("@woile", "@woile")]
395 #[case("10%", "10%")]
396 #[case("#vegan", "#vegan")]
397 #[case("mango's", "mango's")]
398 fn test_parse_valid_string(#[case] input: String, #[case] expected: &str) {
399 let mut input = LocatingSlice::new(input.as_str());
400 let valid_str = parse_valid_string(&mut input).unwrap();
401 assert_eq!(valid_str, expected)
402 }
403
404 #[rstest]
405 #[case("/* */", "")]
406 #[case("/* hello */", "hello")]
407 #[case("/* multi\nline\ncomment */", "multi\nline\ncomment")]
408 fn test_parse_comment_ok(#[case] input: String, #[case] expected: &str) {
409 let mut input = LocatingSlice::new(input.as_str());
410 let comment = parse_comment(&mut input).expect("failed to parse comment");
411 assert_eq!(comment, expected)
412 }
413
414 #[test]
415 fn test_parse_comment_wrong() {
416 let mut input = LocatingSlice::new("/* unclosed");
417 let res = parse_comment(&mut input);
418 assert!(res.is_err());
419
420 let err = res.unwrap_err();
421 println!("{:?}", err);
422 assert!(matches!(err, winnow::error::ErrMode::Cut(_)));
423 }
424
425 #[rstest]
426 #[case("{salt}", "salt")]
427 #[case("{black pepper}", "black pepper")]
428 #[case("{smashed potatoes}", "smashed potatoes")]
429 #[case("{15 minutes}", "15 minutes")]
430 #[case("{ 15 minutes }", "15 minutes")]
431 fn test_parse_curly_ok(#[case] input: String, #[case] expected: &str) {
432 let mut input = LocatingSlice::new(input.as_str());
433 let content = parse_curly(&mut input).expect("to work");
434 assert_eq!(expected, content);
435 }
436
437 #[test]
438 fn test_parse_curly_wrong() {
439 let mut input = LocatingSlice::new("{}");
440 let res = parse_curly(&mut input);
441 assert!(res.is_err());
442
443 let mut input = LocatingSlice::new("{unclosed");
444 let res = parse_curly(&mut input);
445 assert!(res.is_err());
446
447 let err = res.unwrap_err();
448 assert!(matches!(err, winnow::error::ErrMode::Cut(_)));
449 }
450
451 #[rstest]
452 #[case("200", "200")]
453 #[case("2.1", "2.1")]
454 #[case("2_1", "2_1")]
455 #[case("2,1", "2,1")]
456 #[case("2.1", "2.1")]
457 #[case("1/2", "1/2")]
458 #[case(".2", ".2")]
459 fn test_parse_quantity_ok(#[case] input: String, #[case] expected: &str) {
460 let mut input = LocatingSlice::new(input.as_str());
461 let content = parse_quantity(&mut input).expect("to work");
462 assert_eq!(expected, content);
463 }
464
465 #[rstest]
466 #[case("2.")]
467 #[case("2..0")]
468 #[case("2,,0")]
469 #[case("2//0")]
470 fn test_parse_quantity_invalid(#[case] input: String) {
471 let mut input = LocatingSlice::new(input.as_str());
473 let res = parse_quantity(&mut input);
474 let err = res.unwrap_err();
475 assert!(matches!(err, winnow::error::ErrMode::Cut(_)));
476 }
477
478 #[rstest]
479 #[case("(200gr)", (Some("200"), Some("gr")))]
480 #[case("(1/2)", (Some("1/2"), None))]
481 #[case("(100 gr)", (Some("100"), Some("gr")))]
482 #[case("(10 ml)", (Some("10"), Some("ml")))]
483 #[case("( 10 ml )", (Some("10"), Some("ml")))]
484 #[case("(1.5 cups)", (Some("1.5"), Some("cups")))]
485 fn test_parse_ingredient_amount_ok(
486 #[case] input: String,
487 #[case] expected: (Option<&str>, Option<&str>),
488 ) {
489 let mut input = LocatingSlice::new(input.as_str());
490 let content = parse_ingredient_amount(&mut input).expect("to work");
491 assert_eq!(expected, content);
492 }
493
494 #[rstest]
495 #[case("()")]
496 #[case("(unclosed")]
497 fn test_parse_ingredient_amount_invalid_quantity(#[case] input: String) {
498 let mut input = LocatingSlice::new(input.as_str());
499 let res = parse_ingredient_amount(&mut input);
500 match res {
501 Ok(_) => {
502 assert!(false);
504 }
505 Err(e) => match e {
506 winnow::error::ErrMode::Cut(err) => {
507 println!("{}", err);
508 assert_eq!(
509 "expected a quantity value, like 3, 1.2, 1/2 or 1_000",
510 err.to_string()
511 );
512 assert!(true);
513 }
514 _ => {
515 assert!(false);
516 }
517 },
518 }
519 }
520
521 #[rstest]
522 #[case("(1.5")]
523 fn test_parse_ingredient_amount_invalid_amount(#[case] input: String) {
524 let mut input = LocatingSlice::new(input.as_str());
525 let res = parse_ingredient_amount(&mut input);
526 match res {
527 Ok(_) => {
528 assert!(false);
530 }
531 Err(e) => match e {
532 winnow::error::ErrMode::Cut(err) => {
533 println!("{}", err);
534 assert_eq!("expected `)`", err.to_string());
535 assert!(true);
536 }
537 _ => {
538 assert!(false);
539 }
540 },
541 }
542 }
543
544 #[rstest]
545 #[case("{sweet potato}(200gr)", "sweet potato", Some((Some("200"),Some("gr"))))]
546 #[case("{sweet potato}", "sweet potato", None)]
547 fn test_parse_ingredient_ok(
548 #[case] input: String,
549 #[case] expected_ingredient: &str,
550 #[case] expected_amount: Option<(Option<&str>, Option<&str>)>,
551 ) {
552 let mut input = LocatingSlice::new(input.as_str());
553 let (ingredient, amount) = parse_ingredient(&mut input).unwrap();
554 assert_eq!(expected_ingredient, ingredient);
555 assert_eq!(expected_amount, amount);
556 }
557
558 #[rstest]
559 #[case("&{pot}", "pot")]
560 #[case("&{small jar}", "small jar")]
561 #[case("&{stick}", "stick")]
562 #[case("&{bricks}", "bricks")]
563 fn test_parse_material_ok(#[case] input: String, #[case] expected: &str) {
564 let mut input = LocatingSlice::new(input.as_str());
565 let material = parse_material(&mut input).expect("Failed to parse material");
566 assert_eq!(material, expected)
567 }
568
569 #[rstest]
570 #[case("t{1 minute}", "1 minute")]
571 fn test_parse_timer_ok(#[case] input: String, #[case] expected: &str) {
572 let mut input = LocatingSlice::new(input.as_str());
573 let timer = parse_timer(&mut input).expect("Failed to parse timer");
574 assert_eq!(timer, expected)
575 }
576
577 #[rstest]
578 #[case("@{woile/tomato-sauce}(200gr)", "woile/tomato-sauce", Some((Some("200"),Some("gr"))))]
579 #[case("@{woile/tomato-sauce}", "woile/tomato-sauce", None)]
580 #[case("@{special stew}", "special stew", None)]
581 fn test_parse_recipe_ok(
582 #[case] input: String,
583 #[case] expected_recipe: &str,
584 #[case] expected_amount: Option<(Option<&str>, Option<&str>)>,
585 ) {
586 let mut input = LocatingSlice::new(input.as_str());
587 let (recipe, amount) = parse_recipe_ref(&mut input).unwrap();
588 assert_eq!(expected_recipe, recipe);
589 assert_eq!(expected_amount, amount);
590 }
591
592 #[rstest]
593 #[case(">> tags: vegan\n", ("tags", "vegan"))]
594 #[case(">> key: pepe\n", ("key", "pepe"))]
595 #[case(">>key: pepe\n", ("key", "pepe"))]
596 #[case(">> key: pepe\n", ("key", "pepe"))]
597 #[case(">> key: pepe\n", ("key", "pepe"))]
598 #[case(">> key:\t\tpepe\n", ("key", "pepe"))]
599 #[case(">> key:pepe\n", ("key", "pepe"))]
600 fn test_parse_metadata_ok(#[case] input: String, #[case] expected: (&str, &str)) {
601 let mut input = LocatingSlice::new(input.as_str());
602 let metadata = parse_metadata(&mut input).expect("Failed to parse metadata");
603 assert_eq!(metadata, expected)
604 }
605
606 #[rstest]
607 #[case("\n---\nwhat a backstory", "what a backstory")]
608 #[case("\n ---\nwhat a backstory", "what a backstory")]
609 #[case("\n ---\n\nwhat a backstory", "what a backstory")]
610 #[case("\n ---\n\nthis is **markdown**", "this is **markdown**")]
611 #[case("\n ---\n\nthis is [markdown](url)", "this is [markdown](url)")]
612 fn test_parse_backstory_ok(#[case] input: String, #[case] expected: &str) {
613 let mut input = LocatingSlice::new(input.as_str());
614 let backsotry = parse_backstory(&mut input).expect("failed to parse backstory");
615 assert_eq!(backsotry, expected)
616 }
617
618 #[rstest]
619 #[case("\n--- \nwhat a backstory")]
620 fn test_parse_backstory_fail(#[case] input: String) {
621 let mut input = LocatingSlice::new(input.as_str());
622 let out = parse_backstory(&mut input);
623 assert!(out.is_err())
624 }
625
626 #[rstest]
627 #[case(" ", Token::Space(" "))]
628 #[case("{holis}(100 gr)", Token::Ingredient { name: "holis", quantity: Some("100"), unit: Some("gr") })]
629 fn test_recipe_value_ok(#[case] input: &str, #[case] expected: Token) {
630 let mut input = LocatingSlice::new(input);
631 let token = recipe_value(&mut input).expect("failed to parse token");
632 assert_eq!(token, expected)
633 }
634
635 #[test]
636 fn test_recipe_ok() {
637 let input = "Boil the quinoa for t{5 minutes} in a &{pot}.\nPut the boiled {quinoa}(200gr) in the base of the bowl.";
638 let expected = "Boil the quinoa for 5 minutes in a pot.\nPut the boiled quinoa in the base of the bowl.";
639 let recipe = recipe
640 .parse(LocatingSlice::new(input))
641 .expect("parse failed");
642 let fmt_recipe = recipe
643 .iter()
644 .fold(String::new(), |acc, val| format!("{acc}{val}"));
645 println!("{}", fmt_recipe);
646
647 assert_eq!(expected, fmt_recipe);
648 println!("{:?}", recipe);
649 }
650
651 #[rstest]
652 #[case(" ", vec![Token::Space(" ")])]
653 #[case("\n\nhello", vec![Token::Space("\n\n"), Token::Word("hello")])]
654 #[case("hello\n", vec![Token::Word("hello"), Token::Space("\n")])]
655 #[case(">> tags: hello\n\nhello", vec![Token::Metadata {key: "tags", value: "hello"}, Token::Space("\n\n"), Token::Word("hello")])]
656 #[case(">> source: https://hello.com\n>> tags: hello\n", vec![Token::Metadata {key: "source", value: "https://hello.com"}, Token::Space("\n"), Token::Metadata {key: "tags", value: "hello"}, Token::Space("\n")])]
657 #[case("{holis}(100 gr)", vec![Token::Ingredient { name: "holis", quantity: Some("100"), unit: Some("gr") }])]
658 fn test_recipe_cases_ok(#[case] input: &str, #[case] expected: Vec<Token>) {
659 let mut input = LocatingSlice::new(input);
660 let token = recipe(&mut input).expect("failed to parse token");
661 assert_eq!(token, expected)
662 }
663
664 #[rstest]
665 #[case("Foo. ")]
666 #[case("Foo.")]
667 #[case("Foo, bar")]
668 #[case("Foo,bar")]
669 #[case("Foo,")]
670 #[case("Foo, ")]
671 #[case("Foo,\n")]
672 #[case("Foo.\n")]
673 #[case("Foo.\nfoo")]
674 #[case("Foo,\nfoo")]
675 fn test_symbol_parsing(#[case] input: &str) {
676 let recipe_result = parse(input);
677
678 assert!(recipe_result.is_ok());
679 }
680
681 #[test]
682 fn test_parse_ok() {
683 let input = "Boil the quinoa for t{5 minutes} in a &{pot}.\nPut the boiled {quinoa}(200gr) in the base of the bowl.";
684 let expected = "Boil the quinoa for 5 minutes in a pot.\nPut the boiled quinoa in the base of the bowl.";
685 let recipe = parse(input).expect("parse failed");
686 let fmt_recipe = recipe
687 .iter()
688 .fold(String::new(), |acc, val| format!("{acc}{val}"));
689 println!("{}", fmt_recipe);
690
691 assert_eq!(expected, fmt_recipe);
692 println!("{:?}", recipe);
693 }
694
695 #[rstest]
696 #[case("l'{ingredient}", vec![
697 Token::Word("l"),
698 Token::Word("'"),
699 Token::Ingredient {
700 name: "ingredient",
701 quantity: None,
702 unit: None,
703 },
704 ])]
705 #[case("l'&{material}", vec![
706 Token::Word("l"),
707 Token::Word("'"),
708 Token::Material("material"),
709 ])]
710 #[case("l't{mytimer}", vec![
711 Token::Word("l"),
712 Token::Word("'"),
713 Token::Timer("mytimer"),
714 ])]
715 #[case("l`{ingredient}", vec![
716 Token::Word("l"),
717 Token::Word("`"),
718 Token::Ingredient {
719 name: "ingredient",
720 quantity: None,
721 unit: None,
722 },
723 ])]
724 #[case("l`&{material}", vec![
725 Token::Word("l"),
726 Token::Word("`"),
727 Token::Material("material"),
728 ])]
729 #[case("l`t{mytimer}", vec![
730 Token::Word("l"),
731 Token::Word("`"),
732 Token::Timer("mytimer"),
733 ])]
734 fn test_parse_ingredients_without_spaces(#[case] input: &str, #[case] expected: Vec<Token>) {
735 let recipe = parse(input).expect("parse failed");
736
737 println!("{:?}", recipe);
738
739 assert_eq!(expected, recipe);
740 }
741
742 #[test]
743 fn test_parse_with_backstory_ok() {
744 let input = "Foo. \n---\nA backstory";
745 let expected = vec![
746 Token::Word("Foo."),
747 Token::Space(" "),
748 Token::Backstory("A backstory"),
749 ];
750 let recipe = parse(input).expect("parse failed");
751
752 println!("{:?}", recipe);
753
754 assert_eq!(expected, recipe);
755 println!("{:?}", recipe);
756 }
757
758 #[test]
759 #[cfg(feature = "serde")]
760 fn test_token_serialization_works() {
761 let token = Token::Ingredient {
762 name: "quinoa",
763 quantity: Some("200"),
764 unit: Some("gr"),
765 };
766
767 let serialized = serde_json::to_string(&token).expect("failed to serialize");
768 println!("{}", serialized);
769 }
770
771 #[test]
772 #[cfg(feature = "serde")]
773 fn test_token_serialization_creates_right_payload() {
774 let token = Token::Ingredient {
775 name: "quinoa",
776 quantity: Some("200"),
777 unit: Some("gr"),
778 };
779
780 let serialized = serde_json::to_string(&token).expect("failed to serialize");
781 assert_eq!(
782 serialized,
783 r#"{"token":"Ingredient","content":{"name":"quinoa","quantity":"200","unit":"gr"}}"#
784 );
785 }
786
787 #[test]
788 #[cfg(feature = "serde")]
789 fn test_token_serialization_creates_right_payload_single_string() {
790 let token = Token::Word("holis");
791
792 let serialized = serde_json::to_string(&token).expect("failed to serialize");
793 assert_eq!(serialized, r#"{"token":"Word","content":"holis"}"#);
794 }
795
796 #[test]
797 #[cfg(feature = "schemars")]
798 fn test_token_json_schema_generation() {
799 use schemars::schema_for;
800 let schema = schema_for!(Token);
801 println!("{}", serde_json::to_string_pretty(&schema).unwrap());
802 }
803}