pochoir_lang/
lib.rs

1//! # `pochoir`'s minimal language
2//!
3//! <div class="example-wrap">
4//!   <pre class="rust rust-example-rendered"><code><span class="string">"hello "</span> + <span class="string">"world!"</span> |> len() == <span class="number">12</span></code></pre>
5//! </div>
6//!
7//! `pochoir`'s expressions use a hand-crafted, functional and interpreted language used to quickly transform
8//! variables. It is highly functional and aims to keep the code readable while being extremely
9//! compact, that's why there are, for example, no comments. It also has a complete integration and
10//! compatibility with Rust's custom functions which can be defined by using the [`Function`] value, and inserted in the [`Context`]
11//! that's why there are no function declarations.
12//!
13//! ### Types
14//!
15//! Generally, functions and operations are strict about which types can be used. For example, it
16//! is not possible to add a number to a string (e.g `42 + " apples"` does **not work**), you need to
17//! convert the number to a string first using the `to_string` function (e.g `to_string(42) + " apples"` **works**).
18//! The language supports 7 types:
19//!
20//! - [`Null`], a type representing a thing that is not present. It is returned by an empty expression, an undefined variable
21//!   or when indexing an array or an object with a field which does not exist.
22//!   The value having this type is `null`. It is the close to `()` in Rust
23//! - [`Bool`], a type representing a boolean value, can be either `true` or `false`. It is mostly
24//!   returned by equality operators (like `==`). It is the same as [`bool`] in Rust
25//! - [`Number`], a type representing a 64-bit floating point number (like in Javascript). It is the
26//!   same as an [`f64`] in Rust
27//! - [`String`], a type representing an UTF-8 encoded sequence of characters. It is the same as a
28//!   [`String`] in Rust
29//! - [`Array`], a type representing a list of values. It is the same as a [`Vec`] in Rust
30//! - [`Object`], a type representing a key-value pair of values. It is the same as a [`HashMap`](std::collections::HashMap) ordered by order of insertion in Rust
31//! - [`Function`], a type representing a function defined in Rust which can be called in a script.
32//! - [`Range`], a type representing a range of positive numbers. It is the same as a [`Range`] of `i32`s in Rust. It can be created using `..` or `..=`
33//!
34//! [`Null`]: crate::Value::Null
35//! [`Bool`]: crate::Value::Bool
36//! [`Number`]: crate::Value::Number
37//! [`String`]: crate::Value::String
38//! [`Array`]: crate::Value::Array
39//! [`Object`]: crate::Value::Object
40//! [`Function`]: crate::Value::Function
41//! [`Range`]: crate::Value::Range
42//! [`Deserialize`]: serde::Deserialize
43//!
44//! ### Operators
45//!
46//! Because there are no mutable "variables" (only static values passed in the [`Context`] or
47//! intermediate values defined with the `=` operator), it is not possible to mutate values (it is
48//! however possible to redefine them), that's why there are no loops and no mutable assignment
49//! operators (`+=`, `-=`, …). Instead, all the transformations directly return the result. By the
50//! way, it is strongly encouraged to use the pipe operator (`|>`) to give the returned value of a
51//! function as the first argument of the next function instead of having a big soup of
52//! function calls as arguments. For example,
53//! `compute(get_db(request_get("https://crates.io"), "file.txt"))` should be rewritten as
54//! `request_get("https://crates.io") |> get_db("file.txt") |> compute()`.
55//!
56//! The language supports all typical operators:
57//!
58//! - Equality check operators: `==`, `!=`;
59//! - Mathematical operators: `+`, `-`, `/`, `*` (and parentheses to preserve the precedence);
60//! - Comparison operators: `<`, `>`, `<=`, `>=`;
61//! - Logical operators: `&&`, `||`;
62//! - Conditional operator: `<cond> ? <expr_if_true> : <expr_if_false>`.
63//! - Definition operator: `=` (only used to define intermediate constants or to redefine a single value in the context, complex assignments are not supported)
64//!
65//! The types used on each side of the operator **must have the same type**, except [`Null`] values
66//! which can be compared to all other types (only using `==` and `!=`).
67//!
68//! ### Functions and Rust integration
69//!
70//! The language features a tight integration with Rust by declaring functions in Rust and using
71//! them in expressions. Each type passed as argument is automagically deserialized as a Rust type
72//! (using its [`Deserialize`] implementation) and can directly be used. The number of parameters
73//! and their type are checked as well when they are called. The return type **must be**
74//! `FunctionResult<T>` (`FunctionResult<T>` is an alias for `Result<T, Box<dyn std::error::Error>>`)
75//! where `T` implements [`IntoValue`] and is also magically converted into a value (without serializing!)
76//! before executing the rest of the script.
77//!
78//! [`IntoValue`]: crate::IntoValue
79//!
80//! A typical Rust-defined function to convert some emoji codes to their unicode equivalent
81//! can be defined like this:
82//!
83//! ```
84//! use pochoir_lang::{Value, FunctionResult};
85//!
86//! fn emoji(val: String) -> FunctionResult<String> {
87//!     Ok(match val.as_str() {
88//!         ":tada:" => "πŸŽ‰",
89//!         ":rocket:" => "πŸš€",
90//!         _ => return Err(format!("unknown emoji code: `{val}`").into()),
91//!     }.to_string())
92//! }
93//! ```
94//!
95//! And then converted to a function using the [`Function::new`] function and inserted into the
96//! global context using [`Context::insert`]:
97//!
98//! ```
99//! # use pochoir_lang::{Value, FunctionResult};
100//! #
101//! # fn emoji(val: String) -> FunctionResult<String> {
102//! #     Ok(match val.as_str() {
103//! #         ":tada:" => "πŸŽ‰",
104//! #         ":rocket:" => "πŸš€",
105//! #         _ => return Err(format!("unknown emoji code: `{val}`").into()),
106//! #     }.to_string())
107//! # }
108//! use pochoir_lang::{Function, Context};
109//! use pochoir_lang::eval;
110//!
111//! let mut context = Context::new();
112//! context.insert("emoji", Function::new(emoji));
113//!
114//! let content = r#"emoji(":tada:") + emoji(":rocket:")"#;
115//!
116//! assert_eq!(eval("inline-code", content, &mut context, 0).expect("failed to evaluate the code").to_string(), "πŸŽ‰πŸš€".to_string());
117//! ```
118//!
119//! The functions are normal [`Value`]s so they can be namespaced using an object.
120//! For example, you can define a function `length` in the namespace `String` using:
121//!
122//! ```
123//! use pochoir_lang::{Value, Function, Context, object, eval};
124//! use std::error::Error;
125//!
126//! let mut context = Context::new();
127//! context.insert("String", object! {
128//!     "length" => Function::new(|val: String| Ok(val.len())),
129//! });
130//!
131//! let content = r#"String.length('hello')"#;
132//!
133//! assert_eq!(eval("inline-code", content, &mut context, 0).expect("failed to evaluate the code"), Value::Number(5.0));
134//! ```
135//!
136//! You can specify optional parameters using an Option or checking if a [`Value`] is [`Value::Null`], for instance:
137//!
138//! ```
139//! use pochoir_lang::{Function, Context, Value, eval};
140//!
141//! let mut context = Context::new();
142//! context.insert("format_name", Function::new(|firstname: String, surname: Option<String>| {
143//!     if let Some(surname) = surname {
144//!         Ok(format!("{firstname} {surname}"))
145//!     } else {
146//!         Ok(firstname.to_string())
147//!     }
148//! }));
149//!
150//! let content = r#"format_name("Ada") == "Ada" && format_name("Ada", "Lovelace") == "Ada Lovelace""#;
151//!
152//! assert_eq!(eval("inline-code", content, &mut context, 0).expect("failed to evaluate the code"), Value::Bool(true));
153//! ```
154//!
155//! Note that all optional arguments **must be the last arguments** given to a function.
156//! Also note that named arguments are not supported, optional arguments must be given in the order
157//! they are defined.
158//!
159//! #### Default functions
160//!
161//! Some Rust functions are defined by default in the language: check the [`functions`] module to
162//! see all of them.
163//!
164//! ### Opinions
165//!
166//! - The language being used primarily with the rest of `pochoir`'s templating engine, it should be
167//!   able to check if the property of an object or an array (or the attribute of a component) exists
168//!   without raising an error if the property does not exist. That's the *raison d'Γͺtre* of the
169//!   [`Null`] type
170//!
171//! - The language does not support (yet) references, so all arguments given in functions need to
172//!   be owned because functions will manipulate them
173//!
174//! ### Examples
175//!
176//! An expression checking the length of the `content` variable to display the according
177//! adjective. It can, for example, be used to add a quick hint of the length of a blog post content.
178//!
179//! <div class="example-wrap">
180//!   <pre class="rust rust-example-rendered"><code>word_count(content) > <span class="number">100</span> ? word_count(content) > <span class="number">1000</span> ? <span class="string">"very long"</span> : <span class="string">"long"</span> : <span class="string">"short"</span></code></pre>
181//! </div>
182//!
183//! An expression doing some maths. It outputs `507`.
184//!
185//! <div class="example-wrap">
186//!   <pre class="rust rust-example-rendered"><code><span class="number">42</span> * <span class="number">12</span> + (<span class="number">24</span> - <span class="number">6</span>) / ((<span class="number">5</span> - <span class="number">7</span>) * <span class="number">-3</span>)</code></pre>
187//! </div>
188//!
189//! An expression formatting a position given as an array of two elements named `pos`. Following
190//! the value of the `pretty_print` variable, it displays the coordinates using the
191//! `x: <value>, y: <value>` notation or using a tuple.
192//!
193//! <div class="example-wrap">
194//!   <pre class="rust rust-example-rendered"><code>pretty_print
195//!   ? <span class="string">"x: "</span> + to_string(pos[<span class="number">0</span>]) + <span class="string">", y: "</span> + to_string(pos[<span class="number">1</span>])
196//!   : <span class="string">"("</span> + to_string(pos[<span class="number">0</span>]) + <span class="string">", "</span> + to_string(pos[<span class="number">1</span>]) + <span class="string">")"</span></code></pre>
197//! </div>
198//!
199//! ### What is currently supported
200//!
201//! - Support all basic types ([`String`], [`Number`], [`Bool`], [`Array`], [`Object`], [`Function`], [`Range`])
202//! - Support all basic operators (`+`, `-`, `/`, `*`, `==`, `!=`, `<=`, `>=`, `<`, `>`, `&&`, `||`)
203//!   and the order of operations (with parentheses)
204//! - Support defining ranges using syntax like `2..3`, `2..=4`, `..3`, `2..` (just `..` is **not** supported)
205//! - Support the conditional operator (`cond ? expr_if_true : expr_if_false`)
206//! - Support indexing arrays, objects and strings (`array[1]`, `object["key"]`, `object.key`,
207//!   `"hello"[2..4]`, `"world"[3]`, `"hello world"[6..]`)
208//! - Support nesting function calls and the pipe operator (`fn1(fn2("argument"))`, `fn2("argument") |> fn1()`)
209//! - Support defining intermediate constants (with the `=` operator) and redefining values passed
210//!   in the [`Context`] (that's why [`eval`] needs a mutable reference, also with the `=` operator)
211//! - Good Rust functions integration
212//! - Good error reporting
213//!
214//! ### Playground
215//!
216//! A web playground using `WebAssembly` is available at <https://encre-org.gitlab.io/pochoir-playground>
217//! to try the syntax out.
218#![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
267/// Evaluate an expression and returns its return value.
268///
269/// The file offset is useful if you are not evaluating a complete file but just a part of it to
270/// simplify indexing it. It should be 0 if it is the complete file.
271///
272/// # Errors
273///
274/// Returns an error if parsing or interpreting it failed.
275pub 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}