1#![doc(html_logo_url = "https://gitlab.com/encre-org/pochoir/raw/main/.assets/logo.png")]
219#![forbid(unsafe_code)]
220#![warn(
221 missing_debug_implementations,
222 trivial_casts,
223 trivial_numeric_casts,
224 unstable_features,
225 unused_import_braces,
226 unused_qualifications,
227 rustdoc::private_doc_tests,
228 rustdoc::broken_intra_doc_links,
229 rustdoc::private_intra_doc_links,
230 clippy::unnecessary_wraps,
231 clippy::too_many_lines,
232 clippy::string_to_string,
233 clippy::explicit_iter_loop,
234 clippy::unnecessary_cast,
235 clippy::missing_errors_doc,
236 clippy::pedantic,
237 clippy::clone_on_ref_ptr,
238 clippy::non_ascii_literal,
239 clippy::dbg_macro,
240 clippy::map_err_ignore,
241 clippy::use_debug,
242 clippy::map_err_ignore,
243 clippy::use_self,
244 clippy::useless_let_if_seq,
245 clippy::verbose_file_reads,
246 clippy::panic,
247 clippy::unimplemented,
248 clippy::todo
249)]
250#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
251
252pub mod context;
253mod error;
254pub mod functions;
255mod interpreter;
256pub mod parser;
257pub mod value;
258
259use std::path::Path;
260
261pub use context::Context;
262pub use error::{Error, Result};
263pub use functions::{Function, FunctionError, FunctionResult};
264pub use pochoir_macros::*;
265pub use value::{deserialize_from_value, serialize_to_value, FromValue, IntoValue, Object, Value};
266
267pub fn eval<P: AsRef<Path>>(
276 file_path: P,
277 code: &str,
278 context: &mut Context,
279 file_offset: usize,
280) -> Result<Value> {
281 let tokens = parser::parse(file_path, code, file_offset)?;
282 let result = interpreter::interpret(tokens, context)?;
283 Ok(result)
284}
285
286#[cfg(test)]
287mod tests {
288 use pochoir_common::Spanned;
289
290 use super::*;
291
292 #[test]
293 fn doc_example() {
294 assert_eq!(
295 eval(
296 "index.html",
297 r#""hello " + "world!" |> len() == 12"#,
298 &mut Context::new(),
299 0,
300 )
301 .unwrap(),
302 Value::Bool(true)
303 );
304 }
305
306 #[test]
307 fn data() {
308 assert_eq!(
309 eval("index.html", r#""Hello world!""#, &mut Context::new(), 0).unwrap(),
310 Value::String("Hello world!".to_string())
311 );
312 }
313
314 #[test]
315 fn use_variable_test() {
316 let mut context = Context::new();
317 context.insert("hello", "Hello world!");
318
319 assert_eq!(
320 eval("index.html", "hello", &mut context, 0).unwrap(),
321 Value::String("Hello world!".to_string())
322 );
323 }
324
325 #[test]
326 fn use_functions_test() {
327 let mut context = Context::new();
328 context.insert("hello", "Hello world!");
329
330 assert_eq!(
331 eval("index.html", "slugify(hello)", &mut context, 0).unwrap(),
332 Value::String("hello-world".to_string())
333 );
334 }
335
336 #[test]
337 fn define_function_test() {
338 let mut context = Context::new();
339 context.insert("hello", "Hello world!");
340 context.insert(
341 "my_fn",
342 Function::new(|arg1: Value, arg2: Value| {
343 Ok(format!("my_fn called with {arg1:?} and {arg2:?}",))
344 }),
345 );
346
347 assert_eq!(
348 eval("index.html", r#"my_fn("hello", "world")"#, &mut context, 0).unwrap(),
349 Value::String(r#"my_fn called with String("hello") and String("world")"#.to_string())
350 );
351 }
352
353 #[test]
354 fn use_function_argument_types_test() {
355 struct DataNested {
356 life: usize,
357 }
358
359 impl IntoValue for DataNested {
360 fn into_value(self) -> Value {
361 object! {
362 "life" => self.life,
363 }
364 .into_value()
365 }
366 }
367
368 struct Data {
369 some_nested_field: DataNested,
370 }
371
372 impl IntoValue for Data {
373 fn into_value(self) -> Value {
374 object! {
375 "some_nested_field" => self.some_nested_field,
376 }
377 .into_value()
378 }
379 }
380
381 let mut context = Context::new();
382 context.insert("hello", "Hello world!");
383 context.insert(
384 "some_value",
385 Data {
386 some_nested_field: DataNested { life: 12 },
387 },
388 );
389 context.insert(
390 "my_fn",
391 Function::new(
392 |str1: String,
393 str2: String,
394 num1: usize,
395 str3: String,
396 bool1: bool,
397 num2: usize,
398 bool2: bool,
399 str4: String| {
400 Ok(format!("my_fn called with `{str1} {str2} {num1} {str3} {bool1} {num2} {bool2} {str4}`"))
401 },
402 ),
403 );
404
405 assert_eq!(
406 eval("index.html", r#"my_fn("hello", "world", 42, hello, true, some_value.some_nested_field.life, false, "end string")"#, &mut context, 0).unwrap(),
407 Value::String("my_fn called with `hello world 42 Hello world! true 12 false end string`".to_string())
408 );
409 }
410
411 #[test]
412 fn namespaced_function_field_access() {
413 struct RestaurantInfo {
414 name: String,
415 location: String,
416 }
417
418 impl IntoValue for RestaurantInfo {
419 fn into_value(self) -> Value {
420 object! {
421 "name" => self.name,
422 "location" => self.location,
423 }
424 .into_value()
425 }
426 }
427
428 let mut context = Context::new();
429 context.insert(
430 "Restaurants",
431 object! {
432 "get" => Function::new(|name: String| {
433 Ok(RestaurantInfo {
434 name,
435 location: "Oceanside, CA".to_string(),
436 })
437 }),
438 },
439 );
440
441 let code = "Restaurants.get('Killer Pizza from Mars').location == 'Oceanside, CA'";
442
443 assert_eq!(
444 eval("index.html", code, &mut context, 0).expect("failed to evaluate the code"),
445 Value::Bool(true),
446 );
447
448 let code = r#"Restaurants["get"]('Killer Pizza from Mars').location == 'Oceanside, CA'"#;
449
450 assert_eq!(
451 eval("index.html", code, &mut context, 0).expect("failed to evaluate the code"),
452 Value::Bool(true),
453 );
454 }
455
456 #[test]
457 fn objects_test() {
458 #[derive(Debug)]
459 #[allow(dead_code)]
460 struct NestedStructure {
461 nested_field: usize,
462 }
463
464 impl FromValue for NestedStructure {
465 fn from_value(val: Value) -> std::result::Result<Self, Box<dyn std::error::Error>> {
466 if let Value::Object(mut val) = val {
467 Ok(Self {
468 nested_field: usize::from_value(
469 val.remove("nested_field")
470 .ok_or(Error::MissingField("nested_field".to_string()))?,
471 )?,
472 })
473 } else {
474 Err(Box::new(Error::MismatchedTypes {
475 expected: "Object".to_string(),
476 found: val.type_name().to_string(),
477 }))
478 }
479 }
480 }
481
482 #[derive(Debug)]
483 #[allow(dead_code)]
484 struct MyStructure {
485 hello: String,
486 nested: NestedStructure,
487 }
488
489 impl FromValue for MyStructure {
490 fn from_value(val: Value) -> std::result::Result<Self, Box<dyn std::error::Error>> {
491 if let Value::Object(mut val) = val {
492 Ok(Self {
493 hello: String::from_value(
494 val.remove("hello")
495 .ok_or(Error::MissingField("hello".to_string()))?,
496 )?,
497 nested: NestedStructure::from_value(
498 val.remove("nested")
499 .ok_or(Error::MissingField("nested".to_string()))?,
500 )?,
501 })
502 } else {
503 Err(Box::new(Error::MismatchedTypes {
504 expected: "Object".to_string(),
505 found: val.type_name().to_string(),
506 }))
507 }
508 }
509 }
510
511 let mut context = Context::new();
512 context.insert("hello", "Hello world!");
513 context.insert(
514 "my_fn",
515 Function::new(|array: Vec<String>, object: MyStructure| {
516 Ok(format!("my_fn called with `{array:?} {object:?}`"))
517 }),
518 );
519
520 assert_eq!(
521 eval("index.html", r#"my_fn(["hello", "world"], { hello: "world", "nested": { 'nested_field': 42 } })"#, &mut context, 0).unwrap(),
522 Value::String(r#"my_fn called with `["hello", "world"] MyStructure { hello: "world", nested: NestedStructure { nested_field: 42 } }`"#.to_string()),
523 );
524 }
525
526 #[test]
527 fn nested_fn_calls() {
528 let mut context = Context::new();
529 context.insert("hello", "Hello world!");
530
531 context.insert(
532 "fn1",
533 Function::new(|str: String| Ok(format!("fn1 called with `{str}`"))),
534 );
535
536 context.insert(
537 "fn2",
538 Function::new(|num: usize, str: String| Ok(format!("fn2 called with `{num} {str}`"))),
539 );
540
541 context.insert(
542 "fn3",
543 Function::new(|str: String, bool1: bool| {
544 Ok(format!("fn3 called with `{str} {bool1}`"))
545 }),
546 );
547
548 assert_eq!(
549 eval(
550 "index.html",
551 r#"fn3(fn2(42, fn1("hello")), true)"#,
552 &mut context,
553 0
554 )
555 .unwrap(),
556 Value::String(
557 "fn3 called with `fn2 called with `42 fn1 called with `hello`` true`"
558 .to_string()
559 )
560 );
561 }
562
563 #[test]
564 fn chain_functions_test() {
565 let mut context = Context::new();
566 context.insert("hello", "Hello world!");
567
568 context.insert(
569 "fn1",
570 Function::new(|str: String| Ok(format!("fn1 called with `{str}`"))),
571 );
572
573 context.insert(
574 "fn2",
575 Function::new(|base_str: String, num1: usize, num2: usize| {
576 Ok(format!("fn2 called with `{base_str} {num1} {num2}`"))
577 }),
578 );
579
580 context.insert(
581 "fn3",
582 Function::new(|str: String, bool1: bool| {
583 Ok(format!("fn3 called with `{str} {bool1}`"))
584 }),
585 );
586
587 assert_eq!(
588 eval("index.html", r#"fn1("hello \"escape\"") |> fn2(42, 43) |> fn3(true)"#, &mut context, 0).unwrap(),
589 Value::String(r#"fn3 called with `fn2 called with `fn1 called with `hello "escape"` 42 43` true`"#.to_string()),
590 );
591 }
592
593 #[test]
594 fn chain_operators_test() {
595 let mut context = Context::new();
596 context.insert("hello", "Hello world!");
597
598 assert_eq!(
599 eval("index.html",
600 r#"slugify(hello) == "hello-world" ? "This is true with six words" : "This is false" |> word_count() == 6"#,
601 &mut context
602 , 0).unwrap(),
603 Value::Bool(true)
604 );
605 }
606
607 #[test]
608 fn arithmetic_test() {
609 let mut context = Context::new();
610 context.insert("life", 42);
611
612 assert_eq!(
613 eval("index.html", "life + 2 + 1", &mut context, 0).unwrap(),
614 Value::Number(45.0),
615 );
616 }
617
618 #[test]
619 fn mathematical_priority_test() {
620 assert_eq!(
621 eval("index.html", "6/ 2 *(2+1) == 9", &mut Context::new(), 0).unwrap(),
622 Value::Bool(true)
623 );
624 }
625
626 #[test]
627 fn boolean_not_operator() {
628 assert_eq!(
629 eval("index.html", "!false", &mut Context::new(), 0).unwrap(),
630 Value::Bool(true),
631 );
632 }
633
634 #[test]
635 fn additions() {
636 assert_eq!(
637 eval(
638 "index.html",
639 "len('hello' + ' world! ' + to_string(false)) + 2 == 20",
640 &mut Context::new(),
641 0
642 )
643 .unwrap(),
644 Value::Bool(true)
645 );
646
647 assert_eq!(
648 eval(
649 "index.html",
650 r#""a" * 12 == "aaaaaaaaaaaa""#,
651 &mut Context::new(),
652 0
653 )
654 .unwrap(),
655 Value::Bool(true)
656 );
657
658 assert_eq!(
659 eval(
660 "index.html",
661 r#""a" * 12 * 12 == "aaaaaaaaaaaa" * 12"#,
662 &mut Context::new(),
663 0
664 )
665 .unwrap(),
666 Value::Bool(true)
667 );
668 }
669
670 #[test]
671 fn comparison_operator_test() {
672 let mut context = Context::new();
673 context.insert("live", 42);
674
675 assert_eq!(
676 eval(
677 "index.html",
678 "6 <= -2 && 66 < 12 || live > 12 && true",
679 &mut context,
680 0
681 )
682 .unwrap(),
683 Value::Bool(true)
684 );
685 }
686
687 #[test]
688 fn function_comparison() {
689 let mut context = Context::new();
690
691 #[allow(clippy::redundant_closure)]
692 context.insert("compute", Function::new(|arg: Value| Ok(arg)));
693
694 assert_eq!(
695 eval(
696 "index.html",
697 "compute(2 != 2) && compute(2 == 2) |> compute() == false",
698 &mut context,
699 0
700 )
701 .unwrap(),
702 Value::Bool(true)
703 );
704 }
705
706 #[test]
707 fn function_comparison_type_error() {
708 assert_eq!(
709 eval("index.html", "len(2 != 2)", &mut Context::new(), 0),
710 Err(Spanned::new(Error::FunctionError("mismatched types for arguments of the `len` function: expected String or Array, found Bool".to_string())).with_span(4..10).with_file_path("index.html"))
711 );
712 }
713
714 #[test]
715 fn function_with_optional_parameter() {
716 let mut context = Context::new();
717 context.insert(
718 "get",
719 Function::new(|name: Value| {
720 if name == Value::Null {
721 Ok(Value::String("qux".to_string()))
722 } else {
723 Ok(name)
724 }
725 }),
726 );
727
728 assert_eq!(
729 eval("index.html", r#"get("qux") == get()"#, &mut context, 0).unwrap(),
730 Value::Bool(true)
731 );
732
733 let mut context = Context::new();
734 context.insert(
735 "get",
736 Function::new(|name: Option<String>| {
737 if let Some(name) = name {
738 Ok(name)
739 } else {
740 Ok("qux".to_string())
741 }
742 }),
743 );
744
745 assert_eq!(
746 eval("index.html", r#"get("qux") == get()"#, &mut context, 0).unwrap(),
747 Value::Bool(true)
748 );
749 }
750
751 #[test]
752 fn chained_conditional() {
753 let mut context = Context::new();
754 context.insert("content", "a ".repeat(101));
755
756 assert_eq!(
757 eval("index.html", r#"word_count(content) > 100 ? word_count(content) > 1000 ? "very long" : "long" : "short""#, &mut context, 0).unwrap(),
758 Value::String("long".to_string())
759 );
760 }
761
762 #[test]
763 fn complex_test() {
764 let mut context = Context::new();
765 context.insert("life", 42);
766
767 context.insert(
768 "compute",
769 Function::new(|lang_name: String| Ok(lang_name == "Rust")),
770 );
771
772 assert_eq!(
773 eval("index.html", "life * 2 + 3 * 2 == 90 ? compute('Rust') ? \"Rust = \u{2764}\" : 'Nope, Go\\'s better' : compute('js')", &mut context, 0).unwrap(),
774 Value::String("Rust = \u{2764}".to_string()),
775 );
776 }
777
778 #[test]
779 fn array_indexing() {
780 let mut context = Context::new();
781 context.insert("my_arr", vec![1, 2, 3, 4]);
782
783 assert_eq!(
784 eval("index.html", "my_arr[1] == 2.0", &mut context, 0).unwrap(),
785 Value::Bool(true)
786 );
787 }
788
789 #[test]
790 fn array_indexing_out_of_bounds() {
791 let mut context = Context::new();
792 context.insert("my_arr", vec![1, 2, 3, 4]);
793
794 assert_eq!(
795 eval("index.html", "my_arr[12] == null", &mut context, 0).unwrap(),
796 Value::Bool(true)
797 );
798 }
799
800 #[test]
801 fn array_indexing_not_integer() {
802 let mut context = Context::new();
803 context.insert("my_arr", vec![1, 2, 3, 4]);
804
805 assert_eq!(
806 eval("index.html", "my_arr[42.12] == 22", &mut context, 0),
807 Err(
808 Spanned::new(Error::BadArrayIndex("a decimal number".to_string()))
809 .with_span(7..12)
810 .with_file_path("index.html")
811 ),
812 );
813 }
814
815 #[test]
816 fn empty_array_indexing() {
817 let mut context = Context::new();
818 context.insert("my_arr", vec![1, 2, 3, 4]);
819
820 assert_eq!(
821 eval("index.html", "my_arr[] == 22", &mut context, 0),
822 Err(Spanned::new(Error::ExpectedExpression("]".to_string()))
823 .with_span(7..8)
824 .with_file_path("index.html")),
825 );
826 }
827
828 #[test]
829 fn object_array_indexing() {
830 struct DataNested {
831 array: Vec<usize>,
832 }
833
834 impl IntoValue for DataNested {
835 fn into_value(self) -> Value {
836 object! {
837 "array" => self.array,
838 }
839 .into_value()
840 }
841 }
842
843 struct Data {
844 nested: DataNested,
845 }
846
847 impl IntoValue for Data {
848 fn into_value(self) -> Value {
849 object! {
850 "nested" => self.nested,
851 }
852 .into_value()
853 }
854 }
855
856 let mut context = Context::new();
857 context.insert(
858 "my_obj",
859 Data {
860 nested: DataNested {
861 array: vec![1, 2, 3, 4],
862 },
863 },
864 );
865
866 assert_eq!(
867 eval(
868 "index.html",
869 "my_obj.nested.array[1] == 2",
870 &mut context,
871 0
872 )
873 .unwrap(),
874 Value::Bool(true)
875 );
876 }
877
878 #[test]
879 fn object_indexing_with_array_syntax() {
880 struct DataNested {
881 array: Vec<usize>,
882 }
883
884 impl IntoValue for DataNested {
885 fn into_value(self) -> Value {
886 object! {
887 "array" => self.array,
888 }
889 .into_value()
890 }
891 }
892
893 struct Data {
894 nested: DataNested,
895 }
896
897 impl IntoValue for Data {
898 fn into_value(self) -> Value {
899 object! {
900 "nested" => self.nested,
901 }
902 .into_value()
903 }
904 }
905
906 let mut context = Context::new();
907 context.insert(
908 "my_obj",
909 Data {
910 nested: DataNested {
911 array: vec![1, 2, 3, 4],
912 },
913 },
914 );
915
916 assert_eq!(
917 eval(
918 "index.html",
919 r#"my_obj.nested["array"] == [1, 2, 3, 4]"#,
920 &mut context,
921 0,
922 )
923 .unwrap(),
924 Value::Bool(true)
925 );
926 }
927
928 #[test]
929 fn function_and_array_indexing() {
930 let mut context = Context::new();
931 context.insert(
932 "return_array",
933 Function::new(|str: String| {
934 Ok(vec![
935 "hello".into_value(),
936 "world".into_value(),
937 object! { "result" => str }.into_value(),
938 ])
939 }),
940 );
941
942 assert_eq!(
943 eval(
944 "index.html",
945 r#"return_array(['world', 'hello'][1])[2].result == "hello""#,
946 &mut context,
947 0,
948 )
949 .unwrap(),
950 Value::Bool(true)
951 );
952 }
953
954 #[test]
955 fn indexing_by_function_and_array() {
956 assert_eq!(
957 eval(
958 "index.html",
959 "[[1, 2], [3, 4], [5, 6]][len('hi')][[0, 1][0] + 1] == 6",
960 &mut Context::new(),
961 0,
962 )
963 .unwrap(),
964 Value::Bool(true)
965 );
966 }
967
968 #[test]
969 fn nested_array_indexing() {
970 let mut context = Context::new();
971
972 #[rustfmt::skip]
973 context.insert(
974 "my_array",
975 vec![
976 vec![
977 vec![1, 2],
978 vec![3, 4]
979 ],
980 vec![
981 vec![5, 6],
982 vec![7, 8]
983 ],
984 vec![
985 vec![9, 10],
986 vec![11, 12]
987 ]
988 ],
989 );
990
991 assert_eq!(
992 eval("index.html", "my_array[2][1][0] == 11", &mut context, 0).unwrap(),
993 Value::Bool(true)
994 );
995 }
996
997 #[test]
998 fn single_char_string_index() {
999 assert_eq!(
1000 eval("index.html", r#""hello world"[2]"#, &mut Context::new(), 0).unwrap(),
1001 Value::String("l".to_string())
1002 );
1003 }
1004
1005 #[test]
1006 fn complex_function_call() {
1007 let mut context = Context::new();
1008 context.insert("foo", Function::new(|| Ok("foo")));
1009 context.insert("bar", Function::new(|| Ok("bar")));
1010
1011 assert_eq!(
1012 eval("index.html", "[foo, bar][1]()", &mut context, 0).unwrap(),
1013 Value::String("bar".to_string())
1014 );
1015 }
1016
1017 #[test]
1018 fn unary_group() {
1019 assert_eq!(
1020 eval(
1021 "index.html",
1022 "!(2 == 2 && 4 + 2 == 6 && false)",
1023 &mut Context::new(),
1024 0
1025 )
1026 .unwrap(),
1027 Value::Bool(true)
1028 );
1029 }
1030
1031 #[test]
1032 fn define_constant() {
1033 let mut context = Context::new();
1034
1035 assert_eq!(
1036 eval("index.html", "cst = 42", &mut context, 0).unwrap(),
1037 Value::Number(42.0),
1038 );
1039
1040 assert_eq!(context.get("cst"), Some(&Value::Number(42.0)));
1041 }
1042
1043 #[test]
1044 fn define_constant_update_context() {
1045 let mut context = Context::new();
1046 context.insert("foo", "foo");
1047
1048 assert_eq!(
1049 eval("index.html", "foo = 'bar' + ' baz'", &mut context, 0).unwrap(),
1050 Value::String("bar baz".to_string()),
1051 );
1052
1053 assert_eq!(
1054 context.get("foo"),
1055 Some(&Value::String("bar baz".to_string()))
1056 );
1057 }
1058
1059 #[test]
1060 fn define_constant_precedence() {
1061 let mut context = Context::new();
1062 context.insert("my_func", Function::new(|| Ok(vec![1, 2, 3])));
1063
1064 assert_eq!(
1065 eval(
1066 "index.html",
1067 "num = 1 == 1 ? [1, 2, 3] : [4, 5] |> len()",
1068 &mut context,
1069 0
1070 )
1071 .unwrap(),
1072 Value::Number(3.0),
1073 );
1074
1075 assert_eq!(context.get("num"), Some(&Value::Number(3.0)));
1076 }
1077
1078 #[test]
1079 fn define_and_use_constant() {
1080 let mut context = Context::new();
1081 context.insert(
1082 "my_fn",
1083 Function::new(|num_stringified: String, base_num: usize| {
1084 Ok(format!("{num_stringified} likes ({base_num} real)"))
1085 }),
1086 );
1087
1088 assert_eq!(
1089 eval(
1090 "index.html",
1091 "num = 42; num + 13 |> to_string() |> my_fn(num)",
1092 &mut context,
1093 0
1094 )
1095 .unwrap(),
1096 Value::String("55 likes (42 real)".to_string()),
1097 );
1098
1099 assert_eq!(context.get("num"), Some(&Value::Number(42.0)));
1100 }
1101
1102 #[test]
1103 fn define_returns_value() {
1104 let mut context = Context::new();
1105
1106 assert_eq!(
1107 eval("index.html", "(num = 42) * 2", &mut context, 0).unwrap(),
1108 Value::Number(84.0),
1109 );
1110
1111 assert_eq!(context.get("num"), Some(&Value::Number(42.0)));
1112 }
1113
1114 #[test]
1115 fn define_constant_with_expr_error() {
1116 assert_eq!(
1117 eval("index.html", "cst + 2 = 42", &mut Context::new(), 0).unwrap_err(),
1118 Spanned::new(Error::InvalidLeftHandDefinition)
1119 .with_span(0..7)
1120 .with_file_path("index.html"),
1121 );
1122 }
1123
1124 #[test]
1125 fn use_ranges() {
1126 assert_eq!(
1127 eval("index.html", "1 + 2..4", &mut Context::new(), 0).unwrap(),
1128 Value::Range(Some(3), Some(4)),
1129 );
1130
1131 assert_eq!(
1132 eval("index.html", "-5..-2", &mut Context::new(), 0).unwrap(),
1133 Value::Range(Some(-5), Some(-2)),
1134 );
1135
1136 assert_eq!(
1137 eval("index.html", "..=4", &mut Context::new(), 0).unwrap(),
1138 Value::Range(None, Some(5)),
1139 );
1140
1141 assert_eq!(
1142 eval("index.html", r#"len("a").."#, &mut Context::new(), 0).unwrap(),
1143 Value::Range(Some(1), None),
1144 );
1145
1146 assert_eq!(
1147 eval("index.html", r#""a"..3"#, &mut Context::new(), 0).unwrap_err(),
1148 Spanned::new(Error::MismatchedTypes {
1149 expected: "Number".to_string(),
1150 found: "String".to_string(),
1151 })
1152 .with_span(0..3)
1153 .with_file_path("index.html"),
1154 );
1155
1156 assert_eq!(
1157 eval("index.html", "1..3.2", &mut Context::new(), 0).unwrap_err(),
1158 Spanned::new(Error::BadRangeBound("a decimal number".to_string()))
1159 .with_span(3..6)
1160 .with_file_path("index.html"),
1161 );
1162
1163 assert_eq!(
1164 eval("index.html", "[1, 2, 3][1..2]", &mut Context::new(), 0).unwrap(),
1165 Value::Array(vec![2.into_value()]),
1166 );
1167
1168 assert_eq!(
1169 eval("index.html", "'ab'[1..]", &mut Context::new(), 0).unwrap(),
1170 Value::String("b".to_string()),
1171 );
1172
1173 assert_eq!(
1174 eval(
1175 "index.html",
1176 r#""hello"[2..=4] == "llo""#,
1177 &mut Context::new(),
1178 0
1179 )
1180 .unwrap(),
1181 Value::Bool(true),
1182 );
1183 }
1184
1185 #[test]
1186 fn nested_functions() {
1187 let get_object = object! {
1188 "get" => Function::new(|| Ok(vec!["a", "b", "c"])),
1189 };
1190 let fruits_object = object! {
1191 "fruits" => Function::new(move || Ok(get_object.clone())),
1192 };
1193 let mut context = Context::new();
1194 context.insert("shop", fruits_object);
1195
1196 assert_eq!(
1197 eval("index.html", "shop.fruits().get()", &mut context, 0).unwrap(),
1198 Value::Array(vec!["a".into_value(), "b".into_value(), "c".into_value()]),
1199 );
1200 }
1201
1202 #[test]
1203 fn utf8() {
1204 assert_eq!(
1205 eval(
1206 "index.html",
1207 "len('\u{1f389}') == 4",
1208 &mut Context::new(),
1209 0
1210 )
1211 .unwrap(),
1212 Value::Bool(true)
1213 );
1214 }
1215}