#![doc(html_logo_url = "https://gitlab.com/encre-org/pochoir/raw/main/.assets/logo.png")]
#![forbid(unsafe_code)]
#![warn(
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unused_qualifications,
rustdoc::private_doc_tests,
rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links,
clippy::unnecessary_wraps,
clippy::too_many_lines,
clippy::string_to_string,
clippy::explicit_iter_loop,
clippy::unnecessary_cast,
clippy::missing_errors_doc,
clippy::pedantic,
clippy::clone_on_ref_ptr,
clippy::non_ascii_literal,
clippy::dbg_macro,
clippy::map_err_ignore,
clippy::use_debug,
clippy::map_err_ignore,
clippy::use_self,
clippy::useless_let_if_seq,
clippy::verbose_file_reads,
clippy::panic,
clippy::unimplemented,
clippy::todo
)]
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
pub mod context;
mod error;
pub mod functions;
mod interpreter;
pub mod parser;
pub mod value;
use std::path::Path;
pub use context::Context;
pub use error::{Error, Result};
pub use functions::{Function, FunctionError, FunctionResult};
pub use pochoir_macros::*;
pub use value::{deserialize_from_value, serialize_to_value, FromValue, IntoValue, Object, Value};
pub fn eval<P: AsRef<Path>>(
file_path: P,
code: &str,
context: &mut Context,
file_offset: usize,
) -> Result<Value> {
let tokens = parser::parse(file_path, code, file_offset)?;
let result = interpreter::interpret(tokens, context)?;
Ok(result)
}
#[cfg(test)]
mod tests {
use pochoir_common::Spanned;
use super::*;
#[test]
fn doc_example() {
assert_eq!(
eval(
"index.html",
r#""hello " + "world!" |> len() == 12"#,
&mut Context::new(),
0,
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn data() {
assert_eq!(
eval("index.html", r#""Hello world!""#, &mut Context::new(), 0).unwrap(),
Value::String("Hello world!".to_string())
);
}
#[test]
fn use_variable_test() {
let mut context = Context::new();
context.insert("hello", "Hello world!");
assert_eq!(
eval("index.html", "hello", &mut context, 0).unwrap(),
Value::String("Hello world!".to_string())
);
}
#[test]
fn use_functions_test() {
let mut context = Context::new();
context.insert("hello", "Hello world!");
assert_eq!(
eval("index.html", "slugify(hello)", &mut context, 0).unwrap(),
Value::String("hello-world".to_string())
);
}
#[test]
fn define_function_test() {
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert(
"my_fn",
Function::new(|arg1: Value, arg2: Value| {
Ok(format!("my_fn called with {arg1:?} and {arg2:?}",))
}),
);
assert_eq!(
eval("index.html", r#"my_fn("hello", "world")"#, &mut context, 0).unwrap(),
Value::String(r#"my_fn called with String("hello") and String("world")"#.to_string())
);
}
#[test]
fn use_function_argument_types_test() {
struct DataNested {
life: usize,
}
impl IntoValue for DataNested {
fn into_value(self) -> Value {
object! {
"life" => self.life,
}
.into_value()
}
}
struct Data {
some_nested_field: DataNested,
}
impl IntoValue for Data {
fn into_value(self) -> Value {
object! {
"some_nested_field" => self.some_nested_field,
}
.into_value()
}
}
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert(
"some_value",
Data {
some_nested_field: DataNested { life: 12 },
},
);
context.insert(
"my_fn",
Function::new(
|str1: String,
str2: String,
num1: usize,
str3: String,
bool1: bool,
num2: usize,
bool2: bool,
str4: String| {
Ok(format!("my_fn called with `{str1} {str2} {num1} {str3} {bool1} {num2} {bool2} {str4}`"))
},
),
);
assert_eq!(
eval("index.html", r#"my_fn("hello", "world", 42, hello, true, some_value.some_nested_field.life, false, "end string")"#, &mut context, 0).unwrap(),
Value::String("my_fn called with `hello world 42 Hello world! true 12 false end string`".to_string())
);
}
#[test]
fn namespaced_function_field_access() {
struct RestaurantInfo {
name: String,
location: String,
}
impl IntoValue for RestaurantInfo {
fn into_value(self) -> Value {
object! {
"name" => self.name,
"location" => self.location,
}
.into_value()
}
}
let mut context = Context::new();
context.insert(
"Restaurants",
object! {
"get" => Function::new(|name: String| {
Ok(RestaurantInfo {
name,
location: "Oceanside, CA".to_string(),
})
}),
},
);
let code = "Restaurants.get('Killer Pizza from Mars').location == 'Oceanside, CA'";
assert_eq!(
eval("index.html", code, &mut context, 0).expect("failed to evaluate the code"),
Value::Bool(true),
);
let code = r#"Restaurants["get"]('Killer Pizza from Mars').location == 'Oceanside, CA'"#;
assert_eq!(
eval("index.html", code, &mut context, 0).expect("failed to evaluate the code"),
Value::Bool(true),
);
}
#[test]
fn objects_test() {
#[derive(Debug)]
#[allow(dead_code)]
struct NestedStructure {
nested_field: usize,
}
impl FromValue for NestedStructure {
fn from_value(val: Value) -> std::result::Result<Self, Box<dyn std::error::Error>> {
if let Value::Object(mut val) = val {
Ok(Self {
nested_field: usize::from_value(
val.remove("nested_field")
.ok_or(Error::MissingField("nested_field".to_string()))?,
)?,
})
} else {
Err(Box::new(Error::MismatchedTypes {
expected: "Object".to_string(),
found: val.type_name().to_string(),
}))
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
struct MyStructure {
hello: String,
nested: NestedStructure,
}
impl FromValue for MyStructure {
fn from_value(val: Value) -> std::result::Result<Self, Box<dyn std::error::Error>> {
if let Value::Object(mut val) = val {
Ok(Self {
hello: String::from_value(
val.remove("hello")
.ok_or(Error::MissingField("hello".to_string()))?,
)?,
nested: NestedStructure::from_value(
val.remove("nested")
.ok_or(Error::MissingField("nested".to_string()))?,
)?,
})
} else {
Err(Box::new(Error::MismatchedTypes {
expected: "Object".to_string(),
found: val.type_name().to_string(),
}))
}
}
}
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert(
"my_fn",
Function::new(|array: Vec<String>, object: MyStructure| {
Ok(format!("my_fn called with `{array:?} {object:?}`"))
}),
);
assert_eq!(
eval("index.html", r#"my_fn(["hello", "world"], { hello: "world", "nested": { 'nested_field': 42 } })"#, &mut context, 0).unwrap(),
Value::String(r#"my_fn called with `["hello", "world"] MyStructure { hello: "world", nested: NestedStructure { nested_field: 42 } }`"#.to_string()),
);
}
#[test]
fn nested_fn_calls() {
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert(
"fn1",
Function::new(|str: String| Ok(format!("fn1 called with `{str}`"))),
);
context.insert(
"fn2",
Function::new(|num: usize, str: String| Ok(format!("fn2 called with `{num} {str}`"))),
);
context.insert(
"fn3",
Function::new(|str: String, bool1: bool| {
Ok(format!("fn3 called with `{str} {bool1}`"))
}),
);
assert_eq!(
eval(
"index.html",
r#"fn3(fn2(42, fn1("hello")), true)"#,
&mut context,
0
)
.unwrap(),
Value::String(
"fn3 called with `fn2 called with `42 fn1 called with `hello`` true`"
.to_string()
)
);
}
#[test]
fn chain_functions_test() {
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert(
"fn1",
Function::new(|str: String| Ok(format!("fn1 called with `{str}`"))),
);
context.insert(
"fn2",
Function::new(|base_str: String, num1: usize, num2: usize| {
Ok(format!("fn2 called with `{base_str} {num1} {num2}`"))
}),
);
context.insert(
"fn3",
Function::new(|str: String, bool1: bool| {
Ok(format!("fn3 called with `{str} {bool1}`"))
}),
);
assert_eq!(
eval("index.html", r#"fn1("hello \"escape\"") |> fn2(42, 43) |> fn3(true)"#, &mut context, 0).unwrap(),
Value::String(r#"fn3 called with `fn2 called with `fn1 called with `hello "escape"` 42 43` true`"#.to_string()),
);
}
#[test]
fn chain_operators_test() {
let mut context = Context::new();
context.insert("hello", "Hello world!");
assert_eq!(
eval("index.html",
r#"slugify(hello) == "hello-world" ? "This is true with six words" : "This is false" |> word_count() == 6"#,
&mut context
, 0).unwrap(),
Value::Bool(true)
);
}
#[test]
fn arithmetic_test() {
let mut context = Context::new();
context.insert("life", 42);
assert_eq!(
eval("index.html", "life + 2 + 1", &mut context, 0).unwrap(),
Value::Number(45.0),
);
}
#[test]
fn mathematical_priority_test() {
assert_eq!(
eval("index.html", "6/ 2 *(2+1) == 9", &mut Context::new(), 0).unwrap(),
Value::Bool(true)
);
}
#[test]
fn boolean_not_operator() {
assert_eq!(
eval("index.html", "!false", &mut Context::new(), 0).unwrap(),
Value::Bool(true),
);
}
#[test]
fn additions() {
assert_eq!(
eval(
"index.html",
"len('hello' + ' world! ' + to_string(false)) + 2 == 20",
&mut Context::new(),
0
)
.unwrap(),
Value::Bool(true)
);
assert_eq!(
eval(
"index.html",
r#""a" * 12 == "aaaaaaaaaaaa""#,
&mut Context::new(),
0
)
.unwrap(),
Value::Bool(true)
);
assert_eq!(
eval(
"index.html",
r#""a" * 12 * 12 == "aaaaaaaaaaaa" * 12"#,
&mut Context::new(),
0
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn comparison_operator_test() {
let mut context = Context::new();
context.insert("live", 42);
assert_eq!(
eval(
"index.html",
"6 <= -2 && 66 < 12 || live > 12 && true",
&mut context,
0
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn function_comparison() {
let mut context = Context::new();
#[allow(clippy::redundant_closure)]
context.insert("compute", Function::new(|arg: Value| Ok(arg)));
assert_eq!(
eval(
"index.html",
"compute(2 != 2) && compute(2 == 2) |> compute() == false",
&mut context,
0
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn function_comparison_type_error() {
assert_eq!(
eval("index.html", "len(2 != 2)", &mut Context::new(), 0),
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"))
);
}
#[test]
fn function_with_optional_parameter() {
let mut context = Context::new();
context.insert(
"get",
Function::new(|name: Value| {
if name == Value::Null {
Ok(Value::String("qux".to_string()))
} else {
Ok(name)
}
}),
);
assert_eq!(
eval("index.html", r#"get("qux") == get()"#, &mut context, 0).unwrap(),
Value::Bool(true)
);
let mut context = Context::new();
context.insert(
"get",
Function::new(|name: Option<String>| {
if let Some(name) = name {
Ok(name)
} else {
Ok("qux".to_string())
}
}),
);
assert_eq!(
eval("index.html", r#"get("qux") == get()"#, &mut context, 0).unwrap(),
Value::Bool(true)
);
}
#[test]
fn chained_conditional() {
let mut context = Context::new();
context.insert("content", "a ".repeat(101));
assert_eq!(
eval("index.html", r#"word_count(content) > 100 ? word_count(content) > 1000 ? "very long" : "long" : "short""#, &mut context, 0).unwrap(),
Value::String("long".to_string())
);
}
#[test]
fn complex_test() {
let mut context = Context::new();
context.insert("life", 42);
context.insert(
"compute",
Function::new(|lang_name: String| Ok(lang_name == "Rust")),
);
assert_eq!(
eval("index.html", "life * 2 + 3 * 2 == 90 ? compute('Rust') ? \"Rust = \u{2764}\" : 'Nope, Go\\'s better' : compute('js')", &mut context, 0).unwrap(),
Value::String("Rust = \u{2764}".to_string()),
);
}
#[test]
fn array_indexing() {
let mut context = Context::new();
context.insert("my_arr", vec![1, 2, 3, 4]);
assert_eq!(
eval("index.html", "my_arr[1] == 2.0", &mut context, 0).unwrap(),
Value::Bool(true)
);
}
#[test]
fn array_indexing_out_of_bounds() {
let mut context = Context::new();
context.insert("my_arr", vec![1, 2, 3, 4]);
assert_eq!(
eval("index.html", "my_arr[12] == null", &mut context, 0).unwrap(),
Value::Bool(true)
);
}
#[test]
fn array_indexing_not_integer() {
let mut context = Context::new();
context.insert("my_arr", vec![1, 2, 3, 4]);
assert_eq!(
eval("index.html", "my_arr[42.12] == 22", &mut context, 0),
Err(
Spanned::new(Error::BadArrayIndex("a decimal number".to_string()))
.with_span(7..12)
.with_file_path("index.html")
),
);
}
#[test]
fn empty_array_indexing() {
let mut context = Context::new();
context.insert("my_arr", vec![1, 2, 3, 4]);
assert_eq!(
eval("index.html", "my_arr[] == 22", &mut context, 0),
Err(Spanned::new(Error::ExpectedExpression("]".to_string()))
.with_span(7..8)
.with_file_path("index.html")),
);
}
#[test]
fn object_array_indexing() {
struct DataNested {
array: Vec<usize>,
}
impl IntoValue for DataNested {
fn into_value(self) -> Value {
object! {
"array" => self.array,
}
.into_value()
}
}
struct Data {
nested: DataNested,
}
impl IntoValue for Data {
fn into_value(self) -> Value {
object! {
"nested" => self.nested,
}
.into_value()
}
}
let mut context = Context::new();
context.insert(
"my_obj",
Data {
nested: DataNested {
array: vec![1, 2, 3, 4],
},
},
);
assert_eq!(
eval(
"index.html",
"my_obj.nested.array[1] == 2",
&mut context,
0
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn object_indexing_with_array_syntax() {
struct DataNested {
array: Vec<usize>,
}
impl IntoValue for DataNested {
fn into_value(self) -> Value {
object! {
"array" => self.array,
}
.into_value()
}
}
struct Data {
nested: DataNested,
}
impl IntoValue for Data {
fn into_value(self) -> Value {
object! {
"nested" => self.nested,
}
.into_value()
}
}
let mut context = Context::new();
context.insert(
"my_obj",
Data {
nested: DataNested {
array: vec![1, 2, 3, 4],
},
},
);
assert_eq!(
eval(
"index.html",
r#"my_obj.nested["array"] == [1, 2, 3, 4]"#,
&mut context,
0,
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn function_and_array_indexing() {
let mut context = Context::new();
context.insert(
"return_array",
Function::new(|str: String| {
Ok(vec![
"hello".into_value(),
"world".into_value(),
object! { "result" => str }.into_value(),
])
}),
);
assert_eq!(
eval(
"index.html",
r#"return_array(['world', 'hello'][1])[2].result == "hello""#,
&mut context,
0,
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn indexing_by_function_and_array() {
assert_eq!(
eval(
"index.html",
"[[1, 2], [3, 4], [5, 6]][len('hi')][[0, 1][0] + 1] == 6",
&mut Context::new(),
0,
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn nested_array_indexing() {
let mut context = Context::new();
#[rustfmt::skip]
context.insert(
"my_array",
vec![
vec![
vec![1, 2],
vec![3, 4]
],
vec![
vec![5, 6],
vec![7, 8]
],
vec![
vec![9, 10],
vec![11, 12]
]
],
);
assert_eq!(
eval("index.html", "my_array[2][1][0] == 11", &mut context, 0).unwrap(),
Value::Bool(true)
);
}
#[test]
fn single_char_string_index() {
assert_eq!(
eval("index.html", r#""hello world"[2]"#, &mut Context::new(), 0).unwrap(),
Value::String("l".to_string())
);
}
#[test]
fn complex_function_call() {
let mut context = Context::new();
context.insert("foo", Function::new(|| Ok("foo")));
context.insert("bar", Function::new(|| Ok("bar")));
assert_eq!(
eval("index.html", "[foo, bar][1]()", &mut context, 0).unwrap(),
Value::String("bar".to_string())
);
}
#[test]
fn unary_group() {
assert_eq!(
eval(
"index.html",
"!(2 == 2 && 4 + 2 == 6 && false)",
&mut Context::new(),
0
)
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn define_constant() {
let mut context = Context::new();
assert_eq!(
eval("index.html", "cst = 42", &mut context, 0).unwrap(),
Value::Number(42.0),
);
assert_eq!(context.get("cst"), Some(&Value::Number(42.0)));
}
#[test]
fn define_constant_update_context() {
let mut context = Context::new();
context.insert("foo", "foo");
assert_eq!(
eval("index.html", "foo = 'bar' + ' baz'", &mut context, 0).unwrap(),
Value::String("bar baz".to_string()),
);
assert_eq!(
context.get("foo"),
Some(&Value::String("bar baz".to_string()))
);
}
#[test]
fn define_constant_precedence() {
let mut context = Context::new();
context.insert("my_func", Function::new(|| Ok(vec![1, 2, 3])));
assert_eq!(
eval(
"index.html",
"num = 1 == 1 ? [1, 2, 3] : [4, 5] |> len()",
&mut context,
0
)
.unwrap(),
Value::Number(3.0),
);
assert_eq!(context.get("num"), Some(&Value::Number(3.0)));
}
#[test]
fn define_and_use_constant() {
let mut context = Context::new();
context.insert(
"my_fn",
Function::new(|num_stringified: String, base_num: usize| {
Ok(format!("{num_stringified} likes ({base_num} real)"))
}),
);
assert_eq!(
eval(
"index.html",
"num = 42; num + 13 |> to_string() |> my_fn(num)",
&mut context,
0
)
.unwrap(),
Value::String("55 likes (42 real)".to_string()),
);
assert_eq!(context.get("num"), Some(&Value::Number(42.0)));
}
#[test]
fn define_returns_value() {
let mut context = Context::new();
assert_eq!(
eval("index.html", "(num = 42) * 2", &mut context, 0).unwrap(),
Value::Number(84.0),
);
assert_eq!(context.get("num"), Some(&Value::Number(42.0)));
}
#[test]
fn define_constant_with_expr_error() {
assert_eq!(
eval("index.html", "cst + 2 = 42", &mut Context::new(), 0).unwrap_err(),
Spanned::new(Error::InvalidLeftHandDefinition)
.with_span(0..7)
.with_file_path("index.html"),
);
}
#[test]
fn use_ranges() {
assert_eq!(
eval("index.html", "1 + 2..4", &mut Context::new(), 0).unwrap(),
Value::Range(Some(3), Some(4)),
);
assert_eq!(
eval("index.html", "-5..-2", &mut Context::new(), 0).unwrap(),
Value::Range(Some(-5), Some(-2)),
);
assert_eq!(
eval("index.html", "..=4", &mut Context::new(), 0).unwrap(),
Value::Range(None, Some(5)),
);
assert_eq!(
eval("index.html", r#"len("a").."#, &mut Context::new(), 0).unwrap(),
Value::Range(Some(1), None),
);
assert_eq!(
eval("index.html", r#""a"..3"#, &mut Context::new(), 0).unwrap_err(),
Spanned::new(Error::MismatchedTypes {
expected: "Number".to_string(),
found: "String".to_string(),
})
.with_span(0..3)
.with_file_path("index.html"),
);
assert_eq!(
eval("index.html", "1..3.2", &mut Context::new(), 0).unwrap_err(),
Spanned::new(Error::BadRangeBound("a decimal number".to_string()))
.with_span(3..6)
.with_file_path("index.html"),
);
assert_eq!(
eval("index.html", "[1, 2, 3][1..2]", &mut Context::new(), 0).unwrap(),
Value::Array(vec![2.into_value()]),
);
assert_eq!(
eval("index.html", "'ab'[1..]", &mut Context::new(), 0).unwrap(),
Value::String("b".to_string()),
);
assert_eq!(
eval(
"index.html",
r#""hello"[2..=4] == "llo""#,
&mut Context::new(),
0
)
.unwrap(),
Value::Bool(true),
);
}
#[test]
fn nested_functions() {
let get_object = object! {
"get" => Function::new(|| Ok(vec!["a", "b", "c"])),
};
let fruits_object = object! {
"fruits" => Function::new(move || Ok(get_object.clone())),
};
let mut context = Context::new();
context.insert("shop", fruits_object);
assert_eq!(
eval("index.html", "shop.fruits().get()", &mut context, 0).unwrap(),
Value::Array(vec!["a".into_value(), "b".into_value(), "c".into_value()]),
);
}
#[test]
fn utf8() {
assert_eq!(
eval(
"index.html",
"len('\u{1f389}') == 4",
&mut Context::new(),
0
)
.unwrap(),
Value::Bool(true)
);
}
}