221 lines
6.4 KiB
Rust
221 lines
6.4 KiB
Rust
//! Validation functions for template (Liquid) and script (Lua/Python) content.
|
|
//! Per template.allium and script.allium, these are pre-publish gates.
|
|
//!
|
|
//! Current implementation: basic structural checks.
|
|
//! When a full Liquid parser crate is added, upgrade `validate_liquid` to
|
|
//! attempt a real parse. Similarly for `validate_script` with a Lua parser.
|
|
|
|
/// Result of a validation check.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ValidationResult {
|
|
pub valid: bool,
|
|
pub errors: Vec<String>,
|
|
}
|
|
|
|
impl ValidationResult {
|
|
pub fn ok() -> Self {
|
|
Self {
|
|
valid: true,
|
|
errors: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn fail(errors: Vec<String>) -> Self {
|
|
Self {
|
|
valid: false,
|
|
errors,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validate Liquid template content.
|
|
/// Per template.allium: "LiquidJS parser must accept the template".
|
|
/// Currently performs structural tag-matching. Upgrade to full liquid crate parse later.
|
|
pub fn validate_liquid(content: &str) -> ValidationResult {
|
|
let mut errors = Vec::new();
|
|
|
|
// Check for unmatched Liquid block tags
|
|
let block_tags = [
|
|
"if", "unless", "for", "case", "capture", "comment", "raw", "paginate", "tablerow",
|
|
"block", "schema",
|
|
];
|
|
|
|
for tag in &block_tags {
|
|
let open_pattern = format!("{{% {tag}");
|
|
let close_pattern = format!("{{% end{tag}");
|
|
let opens = content.matches(&open_pattern).count();
|
|
let closes = content.matches(&close_pattern).count();
|
|
if opens != closes {
|
|
errors.push(format!(
|
|
"Unmatched {{% {tag} %}}: {opens} opens, {closes} closes"
|
|
));
|
|
}
|
|
}
|
|
|
|
// Check for unclosed {{ }}
|
|
let double_opens = content.matches("{{").count();
|
|
let double_closes = content.matches("}}").count();
|
|
if double_opens != double_closes {
|
|
errors.push(format!(
|
|
"Unmatched {{}}: {double_opens} opens, {double_closes} closes"
|
|
));
|
|
}
|
|
|
|
// Check for unclosed {% %}
|
|
let tag_opens = content.matches("{%").count();
|
|
let tag_closes = content.matches("%}").count();
|
|
if tag_opens != tag_closes {
|
|
errors.push(format!(
|
|
"Unmatched {{% %}}: {tag_opens} opens, {tag_closes} closes"
|
|
));
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
ValidationResult::ok()
|
|
} else {
|
|
ValidationResult::fail(errors)
|
|
}
|
|
}
|
|
|
|
/// Validate script content (Lua syntax).
|
|
/// Per script.allium: "AST parsing must succeed".
|
|
/// Currently performs basic bracket-matching. Upgrade to full Lua parser later.
|
|
pub fn validate_script(content: &str) -> ValidationResult {
|
|
let mut errors = Vec::new();
|
|
|
|
// Basic bracket matching
|
|
let mut paren_depth: i32 = 0;
|
|
let mut brace_depth: i32 = 0;
|
|
let mut bracket_depth: i32 = 0;
|
|
|
|
for (line_num, line) in content.lines().enumerate() {
|
|
// Skip comment lines
|
|
let trimmed = line.trim();
|
|
if trimmed.starts_with("--") {
|
|
continue;
|
|
}
|
|
for ch in line.chars() {
|
|
match ch {
|
|
'(' => paren_depth += 1,
|
|
')' => paren_depth -= 1,
|
|
'{' => brace_depth += 1,
|
|
'}' => brace_depth -= 1,
|
|
'[' => bracket_depth += 1,
|
|
']' => bracket_depth -= 1,
|
|
_ => {}
|
|
}
|
|
if paren_depth < 0 {
|
|
errors.push(format!("Unexpected ')' at line {}", line_num + 1));
|
|
paren_depth = 0;
|
|
}
|
|
if brace_depth < 0 {
|
|
errors.push(format!("Unexpected '}}' at line {}", line_num + 1));
|
|
brace_depth = 0;
|
|
}
|
|
if bracket_depth < 0 {
|
|
errors.push(format!("Unexpected ']' at line {}", line_num + 1));
|
|
bracket_depth = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if paren_depth != 0 {
|
|
errors.push(format!("Unclosed parentheses: depth {paren_depth}"));
|
|
}
|
|
if brace_depth != 0 {
|
|
errors.push(format!("Unclosed braces: depth {brace_depth}"));
|
|
}
|
|
if bracket_depth != 0 {
|
|
errors.push(format!("Unclosed brackets: depth {bracket_depth}"));
|
|
}
|
|
|
|
// Check for unmatched Lua block keywords.
|
|
// In Lua, `for ... do ... end` and `while ... do ... end` each form a
|
|
// single block, so `do` on such lines must NOT be counted as a separate
|
|
// opener.
|
|
let mut block_opens: usize = 0;
|
|
let mut block_closes: usize = 0;
|
|
for line in content.lines() {
|
|
let trimmed = line.trim();
|
|
if trimmed.starts_with("--") {
|
|
continue;
|
|
}
|
|
let words: Vec<&str> = trimmed.split_whitespace().collect();
|
|
let has_for_or_while = words.iter().any(|w| matches!(*w, "for" | "while"));
|
|
for w in &words {
|
|
match *w {
|
|
"function" | "if" | "for" | "while" | "repeat" => block_opens += 1,
|
|
"do" if !has_for_or_while => block_opens += 1,
|
|
"end" | "until" => block_closes += 1,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if block_opens > block_closes {
|
|
errors.push(format!(
|
|
"Possible unclosed blocks: {block_opens} openers, {block_closes} closers"
|
|
));
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
ValidationResult::ok()
|
|
} else {
|
|
ValidationResult::fail(errors)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn valid_liquid_template() {
|
|
let content = r#"
|
|
<html>
|
|
{% if user %}
|
|
<p>{{ user.name }}</p>
|
|
{% endif %}
|
|
</html>"#;
|
|
let result = validate_liquid(content);
|
|
assert!(result.valid, "Errors: {:?}", result.errors);
|
|
}
|
|
|
|
#[test]
|
|
fn unmatched_liquid_if() {
|
|
let content = "{% if true %}<p>Hello</p>";
|
|
let result = validate_liquid(content);
|
|
assert!(!result.valid);
|
|
assert!(result.errors.iter().any(|e| e.contains("if")));
|
|
}
|
|
|
|
#[test]
|
|
fn unmatched_liquid_output() {
|
|
let content = "{{ user.name";
|
|
let result = validate_liquid(content);
|
|
assert!(!result.valid);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_lua_script() {
|
|
let content = r#"
|
|
function render(ctx)
|
|
local result = {}
|
|
for i = 1, 10 do
|
|
result[i] = i * 2
|
|
end
|
|
return result
|
|
end
|
|
"#;
|
|
let result = validate_script(content);
|
|
assert!(result.valid, "Errors: {:?}", result.errors);
|
|
}
|
|
|
|
#[test]
|
|
fn unmatched_lua_parens() {
|
|
let content = "function test(\n local x = 1\nend";
|
|
let result = validate_script(content);
|
|
assert!(!result.valid);
|
|
}
|
|
}
|