use std::sync::LazyLock;
use regex::Regex;
use serde_json::Value;
use super::error::{ValidationError, ValidationReport};
use super::ref_fields::RefFieldSpec;
pub const MAX_GLOBAL_DEPTH: u32 = 50;
pub const MAX_FUNC_CALL_DEPTH: u32 = 5;
static RELAXED_PATH_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?:(?:/(?:[^~/]|~[01])*)*|(?:[^~/]|~[01])+(?:/(?:[^~/]|~[01])*)*)$")
.expect("RELAXED_PATH_PATTERN is a compile-time-constant regex")
});
pub fn get_component_references(component: &Value, spec: &RefFieldSpec) -> Vec<(String, String)> {
let mut refs = Vec::new();
let Some(obj) = component.as_object() else {
return refs;
};
for &key in spec.single_refs {
if let Some(s) = obj.get(key).and_then(|v| v.as_str()) {
refs.push((s.to_string(), key.to_string()));
}
}
for &key in spec.list_refs {
let Some(val) = obj.get(key) else {
continue;
};
match val {
Value::Array(arr) => {
for (i, item) in arr.iter().enumerate() {
if let Some(s) = item.as_str() {
refs.push((s.to_string(), format!("{key}[{i}]")));
}
}
}
Value::Object(o) => {
if let Some(cid) = o.get("componentId").and_then(|v| v.as_str()) {
refs.push((cid.to_string(), format!("{key}.componentId")));
}
}
_ => {}
}
}
refs
}
pub fn validate_component_integrity(
components: &[Value],
spec: &RefFieldSpec,
root_id: &str,
allow_dangling_references: bool,
allow_missing_root: bool,
) -> ValidationReport {
let mut report = ValidationReport::new();
let mut ids: std::collections::HashSet<String> = std::collections::HashSet::new();
for comp in components {
let Some(comp_id) = comp.as_object().and_then(|o| o.get("id")).and_then(|v| v.as_str()) else {
continue;
};
if !ids.insert(comp_id.to_string()) {
report.push(ValidationError::duplicate_id(comp_id));
}
}
if allow_dangling_references {
return report;
}
if !allow_missing_root && !ids.contains(root_id) {
report.push(ValidationError::missing_root(root_id));
}
for comp in components {
let comp_id = comp
.as_object()
.and_then(|o| o.get("id"))
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
for (ref_id, field) in get_component_references(comp, spec) {
if !ids.contains(&ref_id) {
report.push(ValidationError::dangling(comp_id, &ref_id, &field));
}
}
}
report
}
pub fn validate_recursion_and_paths(data: &Value) -> ValidationReport {
let mut report = ValidationReport::new();
traverse(data, 0, 0, &mut report);
report
}
fn traverse(item: &Value, global_depth: u32, func_depth: u32, report: &mut ValidationReport) {
if global_depth > MAX_GLOBAL_DEPTH {
report.push(ValidationError::global_depth("<anon>"));
return;
}
match item {
Value::Array(arr) => {
for x in arr {
traverse(x, global_depth + 1, func_depth, report);
}
}
Value::Object(obj) => {
if let Some(p) = obj.get("path").and_then(|v| v.as_str()) {
if !RELAXED_PATH_PATTERN.is_match(p) {
report.push(ValidationError::invalid_path(p));
}
}
let is_func_v09 = obj.get("call").is_some() && obj.get("args").is_some();
if is_func_v09 {
if func_depth >= MAX_FUNC_CALL_DEPTH {
report.push(ValidationError::func_depth());
for (k, v) in obj {
if k == "args" {
traverse(v, global_depth + 1, func_depth + 1, report);
} else {
traverse(v, global_depth + 1, func_depth, report);
}
}
return;
}
for (k, v) in obj {
if k == "args" {
traverse(v, global_depth + 1, func_depth + 1, report);
} else {
traverse(v, global_depth + 1, func_depth, report);
}
}
} else {
for v in obj.values() {
traverse(v, global_depth + 1, func_depth, report);
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate::error::ValidationErrorCode;
use crate::validate::ref_fields::RefFieldSpec;
use serde_json::json;
fn spec() -> RefFieldSpec {
RefFieldSpec::DEFAULT
}
#[test]
fn refs_extract_child_string() {
let comp = json!({ "id": "c1", "component": "Box", "child": "child1" });
let refs = get_component_references(&comp, &spec());
assert!(refs.iter().any(|(r, _)| r == "child1"));
}
#[test]
fn refs_extract_children_array() {
let comp = json!({ "id": "c1", "component": "Column", "children": ["a", "b"] });
let refs = get_component_references(&comp, &spec());
let ids: Vec<&str> = refs.iter().map(|(r, _)| r.as_str()).collect();
assert!(ids.contains(&"a"));
assert!(ids.contains(&"b"));
}
#[test]
fn refs_extract_children_template_component_id() {
let comp = json!({
"id": "c1", "component": "Column",
"children": { "componentId": "card", "path": "/items" }
});
let refs = get_component_references(&comp, &spec());
assert!(refs.iter().any(|(r, f)| r == "card" && f == "children.componentId"));
}
#[test]
fn refs_extract_active_tab() {
let comp = json!({ "id": "c1", "component": "Tabs", "activeTab": "tab1" });
let refs = get_component_references(&comp, &spec());
assert!(refs.iter().any(|(r, _)| r == "tab1"));
}
#[test]
fn integrity_valid_no_errors() {
let components = vec![
json!({ "id": "root", "component": "Column", "children": ["c1"] }),
json!({ "id": "c1", "component": "Text", "text": "hi" }),
];
let r = validate_component_integrity(&components, &spec(), "root", false, false);
assert!(r.is_empty(), "expected no errors, got: {r}");
}
#[test]
fn integrity_duplicate_id() {
let components = vec![
json!({ "id": "c1", "component": "Box" }),
json!({ "id": "c1", "component": "Text" }),
];
let r = validate_component_integrity(&components, &spec(), "root", false, true);
assert!(r.has_code(&ValidationErrorCode::DuplicateId));
}
#[test]
fn integrity_missing_root() {
let components = vec![json!({ "id": "c1", "component": "Box" })];
let r = validate_component_integrity(&components, &spec(), "root", false, false);
assert!(r.has_code(&ValidationErrorCode::MissingRoot));
}
#[test]
fn integrity_dangling_ref() {
let components =
vec![json!({ "id": "root", "component": "Box", "child": "nonexistent" })];
let r = validate_component_integrity(&components, &spec(), "root", false, false);
assert!(r.has_code(&ValidationErrorCode::DanglingReference));
}
#[test]
fn recursion_valid_path() {
let data = json!({ "path": "/valid/path", "nested": [{ "path": "/another" }] });
let r = validate_recursion_and_paths(&data);
assert!(r.is_empty(), "expected no errors, got: {r}");
}
#[test]
fn recursion_invalid_path_syntax() {
let data = json!({ "path": "invalid~path//double" });
let r = validate_recursion_and_paths(&data);
assert!(r.has_code(&ValidationErrorCode::InvalidPathSyntax));
}
#[test]
fn recursion_global_depth_exceeded() {
let mut deep = json!(null);
for _ in 0..52 {
deep = json!([deep]);
}
let r = validate_recursion_and_paths(&deep);
assert!(r.has_code(&ValidationErrorCode::GlobalDepthExceeded));
}
#[test]
fn recursion_func_call_depth_exceeded() {
let mut deep = json!({});
for _ in 0..6 {
deep = json!({ "call": "func", "args": deep });
}
let r = validate_recursion_and_paths(&deep);
assert!(r.has_code(&ValidationErrorCode::FuncCallDepthExceeded));
}
#[test]
fn relaxed_allows_dangling_and_missing_root() {
let components =
vec![json!({ "id": "root", "component": "Box", "child": "ghost" })];
let r = validate_component_integrity(&components, &spec(), "root", true, true);
assert!(r.is_empty(), "expected no errors under RELAXED, got: {r}");
}
}