use crate::error::ErrorCode;
use crate::path::{PathToken, parse_path};
use crate::v2_model::V2Ref;
use super::{V2Scope, V2ValidationCtx};
pub fn validate_v2_ref(
v2_ref: &V2Ref,
base_path: &str,
scope: &V2Scope,
ctx: &mut V2ValidationCtx<'_>,
) {
match v2_ref {
V2Ref::Input(path) => {
validate_path_syntax(path, base_path, ctx);
}
V2Ref::Context(path) => {
validate_path_syntax(path, base_path, ctx);
ctx.context_referenced = true;
}
V2Ref::Out(path) => {
validate_path_syntax(path, base_path, ctx);
validate_out_not_forward(path, base_path, ctx);
}
V2Ref::Pipe(path) => {
if !scope.allows_pipe() {
ctx.push_error(
ErrorCode::InvalidRefNamespace,
"$ refs are only valid inside pipe steps or custom op bodies",
base_path,
);
} else {
validate_path_syntax(path, base_path, ctx);
}
}
V2Ref::Item(path) => {
if !scope.allows_item() {
ctx.push_error(
ErrorCode::InvalidItemRef,
"@item is only valid inside map/filter operations",
base_path,
);
} else {
validate_item_path(path, base_path, ctx);
}
}
V2Ref::Acc(path) => {
if !scope.allows_acc() {
ctx.push_error(
ErrorCode::InvalidAccRef,
"@acc is only valid inside reduce/fold operations",
base_path,
);
} else if !path.is_empty() {
validate_path_syntax(path, base_path, ctx);
}
}
V2Ref::Local(name) => {
if !scope.has_binding(name) {
ctx.push_error(
ErrorCode::UndefinedVariable,
format!("undefined variable: @{}", name),
base_path,
);
}
}
}
}
fn validate_path_syntax(path: &str, base_path: &str, ctx: &mut V2ValidationCtx<'_>) {
if path.is_empty() {
return; }
if parse_path(path).is_err() {
ctx.push_error(ErrorCode::InvalidPath, "invalid path syntax", base_path);
}
}
fn validate_item_path(path: &str, base_path: &str, ctx: &mut V2ValidationCtx<'_>) {
if path.is_empty() {
return; }
if path == "index" || path == "value" {
return; }
validate_path_syntax(path, base_path, ctx);
}
fn validate_out_not_forward(path: &str, base_path: &str, ctx: &mut V2ValidationCtx<'_>) {
if ctx.allow_any_out_ref {
return;
}
if path.is_empty() {
return;
}
let tokens = match parse_path(path) {
Ok(t) => t,
Err(_) => return, };
let key_tokens: Vec<PathToken> = tokens
.iter()
.filter_map(|t| match t {
PathToken::Key(k) => Some(PathToken::Key(k.clone())),
PathToken::Index(_) => None,
})
.collect();
if key_tokens.is_empty() {
ctx.push_error(
ErrorCode::ForwardOutReference,
"out reference must have at least one key",
base_path,
);
return;
}
let mut candidate = key_tokens;
while !candidate.is_empty() {
if ctx.produced_targets.contains(&candidate) {
return; }
candidate.pop();
}
ctx.push_error(
ErrorCode::ForwardOutReference,
"out reference must point to previous mappings",
base_path,
);
}
#[cfg(test)]
mod tests;