custom salt-like function syntax: function['module.method']('parameter')
?
#668
-
Hi there Firstly, I want to apologize for this type of long question and I might present myself as a total idiot in terms of programming. Anyway, I've stumbled across minijinja in search for a templating engine for jinja and so far, it seems promising to me. Now I wanted to go a step further and try to achieve a function in the salt-syntax to call functions, which I figured out is not native to jinja and something customly made. For anyone asking what's the "salt-syntax": ...
{% if salt['file.file_exists']("path/to/file") %}
... I've looked through it and also asked Claude (I know, many of you might disagree using an AI for coding) and as we all know, AI's are not the best in terms of programming. They use structs or methods which do not exist, which results in non-functioning code. However, I think I might have learned some theory, on how I might achieve it. Anyway, I'll happily post the code, Claude has generated: MiniJinja Salt Syntax with Argumentsuse minijinja::lexer::{Token, TokenKind};
use minijinja::parser::{Expression, ExpressionKind};
use minijinja::{Environment, Error, ErrorKind, State, Value};
use std::collections::HashMap;
use std::path::Path;
// Enhanced SaltExpression to include arguments
#[derive(Debug, Clone)]
struct SaltExpression {
module: String,
function: String,
args: Vec<Expression>,
kwargs: HashMap<String, Expression>,
}
struct SaltParser;
impl SaltParser {
fn parse(tokens: &[Token]) -> Result<Expression, Error> {
let mut iter = tokens.iter().peekable();
// Parse 'salt'
match iter.next() {
Some(token) if token.value() == "salt" => {},
_ => return Err(Error::new(ErrorKind::SyntaxError, "Expected 'salt'")),
}
// Parse '['
match iter.next() {
Some(token) if token.value() == "[" => {},
_ => return Err(Error::new(ErrorKind::SyntaxError, "Expected '['")),
}
// Parse module name
let module = match iter.next() {
Some(token) if token.kind() == TokenKind::Identifier => token.value().to_string(),
_ => return Err(Error::new(ErrorKind::SyntaxError, "Expected module name")),
};
// Parse '.'
match iter.next() {
Some(token) if token.value() == "." => {},
_ => return Err(Error::new(ErrorKind::SyntaxError, "Expected '.'")),
}
// Parse function name
let function = match iter.next() {
Some(token) if token.kind() == TokenKind::Identifier => token.value().to_string(),
_ => return Err(Error::new(ErrorKind::SyntaxError, "Expected function name")),
};
// Parse ']'
match iter.next() {
Some(token) if token.value() == "]" => {},
_ => return Err(Error::new(ErrorKind::SyntaxError, "Expected ']'")),
}
// Parse arguments if present
let mut args = Vec::new();
let mut kwargs = HashMap::new();
// Check for opening parenthesis for arguments
if iter.peek().map(|t| t.value()) == Some("(") {
iter.next(); // consume '('
while let Some(token) = iter.peek() {
if token.value() == ")" {
iter.next(); // consume ')'
break;
}
// Parse keyword arguments
if let Some(next_token) = iter.peek().map(|t| t.value()) {
if next_token.contains('=') {
let kw_parts: Vec<&str> = next_token.split('=').collect();
if kw_parts.len() == 2 {
let key = kw_parts[0].to_string();
let value = Expression::new_const(Value::from(kw_parts[1]), (0, 0));
kwargs.insert(key, value);
}
iter.next(); // consume the keyword arg
} else {
// Parse positional argument
args.push(Expression::new_const(Value::from(next_token), (0, 0)));
iter.next(); // consume the arg
}
}
// Handle comma separator
if iter.peek().map(|t| t.value()) == Some(",") {
iter.next(); // consume ','
}
}
}
Ok(Expression::new_custom(
SaltExpression {
module,
function,
args,
kwargs,
},
(0, tokens.len()),
))
}
}
// Enhanced Module implementation with more realistic functions
struct Module {
functions: HashMap<String, Box<dyn Fn(Vec<Value>, HashMap<String, Value>) -> Result<Value, Error> + Send + Sync>>,
}
impl Module {
fn new() -> Self {
Module {
functions: HashMap::new(),
}
}
fn add_function<F>(&mut self, name: &str, f: F)
where
F: Fn(Vec<Value>, HashMap<String, Value>) -> Result<Value, Error> + Send + Sync + 'static,
{
self.functions.insert(name.to_string(), Box::new(f));
}
}
// Enhanced evaluator to handle arguments
fn evaluate_salt_expression(expr: &SaltExpression, state: &State) -> Result<Value, Error> {
let modules = state
.lookup("modules")
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "modules not found in state"))?;
let module = modules
.get_item(&expr.module)
.map_err(|_| Error::new(
ErrorKind::InvalidOperation,
format!("module '{}' not found", expr.module)
))?;
let function = module
.get_attr("functions")
.and_then(|f| f.get_item(&expr.function))
.map_err(|_| Error::new(
ErrorKind::InvalidOperation,
format!("function '{}' not found in module '{}'", expr.function, expr.module)
))?;
// Evaluate positional arguments
let mut args = Vec::new();
for arg_expr in &expr.args {
args.push(arg_expr.eval(state)?);
}
// Evaluate keyword arguments
let mut kwargs = HashMap::new();
for (key, value_expr) in &expr.kwargs {
kwargs.insert(key.clone(), value_expr.eval(state)?);
}
function.call(&[Value::from(args), Value::from(kwargs)])
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut env = Environment::new();
// Register custom syntax
env.add_syntax_extension(Box::new(move |tokens| {
if tokens.first().map(|t| t.value()) == Some("salt") {
SaltParser::parse(tokens)
} else {
Ok(Expression::dummy())
}
}));
// Create file operations module
let mut file_module = Module::new();
// Add file.exists function
file_module.add_function("exists", |args, kwargs| {
let path = if !args.is_empty() {
args[0].as_str().unwrap_or_default()
} else if let Some(path) = kwargs.get("path") {
path.as_str().unwrap_or_default()
} else {
return Err(Error::new(ErrorKind::InvalidOperation, "No path provided"));
};
Ok(Value::from(Path::new(path).exists()))
});
// Add file.is_file function
file_module.add_function("is_file", |args, kwargs| {
let path = if !args.is_empty() {
args[0].as_str().unwrap_or_default()
} else if let Some(path) = kwargs.get("path") {
path.as_str().unwrap_or_default()
} else {
return Err(Error::new(ErrorKind::InvalidOperation, "No path provided"));
};
Ok(Value::from(Path::new(path).is_file()))
});
// Create modules map
let mut modules = HashMap::new();
modules.insert("file".to_string(), Value::from_serializable(&file_module));
// Create template context
let ctx = minijinja::context! {
modules => modules,
};
// Test the template with different argument styles
let templates = vec![
r#"{{ salt[file.exists]("/path/to/file") }}"#,
r#"{{ salt[file.exists](path="/path/to/file") }}"#,
r#"{{ salt[file.is_file]("/path/to/file") }}"#,
];
for template in templates {
let tmpl = env.template_from_str(template)?;
let result = tmpl.render(ctx.clone())?;
println!("Template: {}\nResult: {}\n", template, result);
}
Ok(())
} For me, it looks currently like there has to be a less complex way to achieve my goal, which lead me to asking the community here. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
use std::collections::BTreeMap;
use minijinja::{Environment, Value};
fn file_exists(filename: &str) -> bool {
std::fs::metadata(filename).map_or(false, |x| x.is_file())
}
fn main() {
let mut env = Environment::new();
let salt_module = BTreeMap::from([(
"file.file_exists".to_string(),
Value::from_function(file_exists),
)]);
env.add_global("salt", Value::from_object(salt_module));
env.add_template(
"test.txt",
"File exists: {{ salt['file.file_exists']('src/main.rs') }}",
)
.unwrap();
let template = env.get_template("test.txt").unwrap();
println!("{}", template.render(()).unwrap());
} Alternatively to a |
Beta Was this translation helpful? Give feedback.
env.add_function
is just an alias forenv.add_global
withValue::from_function
. This means you can just create a map and assign a value to it: