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