use super::*;
pub fn graph_scope<'a>(g: impl IntoAnyGraph) -> Scope<'a> {
graph_scope_with_options(g, &[], Arc::new(crate::graph::AllowAll))
}
pub fn graph_scope_with_options<'a>(
g: impl IntoAnyGraph,
options: &[(&str, ElementProperty)],
authorizer: Arc<dyn crate::graph::Authorizer>,
) -> Scope<'a> {
let access = Arc::new(access::ScriptAccess::new(authorizer, options));
let mut source = ScriptGraphSource::graph(g).with_access(access.clone(), false);
for (key, value) in options {
source
.options
.insert(CompactString::new(key), value.clone());
}
scope_with_access(source, access)
}
pub(super) fn scope_with_access<'a>(
g: ScriptGraphTraversalSource,
access: Arc<access::ScriptAccess>,
) -> Scope<'a> {
let scratch = |anonymous: bool| {
let mut source = ScriptGraphSource::empty().with_access(access.clone(), true);
source.anonymous = anonymous;
for (key, value) in access.presets.iter() {
source.options.insert(key.clone(), value.clone());
}
source
};
let mut scope: Scope = Scope::new();
scope.push("g", g);
scope.push("__", scratch(true));
scope.push("_g", scratch(false));
scope.push("P", ScriptP);
scope.push("GType", ScriptGType);
scope.push("UUID", ScriptUuidTokens);
scope.push("Set", ScriptSetTokens);
scope.push("Column", ScriptColumn);
scope.push("keys", Column::Keys);
scope.push("values", Column::Values);
scope.push("TextP", ScriptTextP);
scope.push("V", ScriptV);
scope.push("By", ScriptBy);
scope.push("CastPolicy", ScriptCastPolicy);
scope.push("ProfileType", ScriptProfileType);
scope.push("Order", ScriptOrder);
scope.push("Pick", ScriptPick);
scope.push("T", ScriptT);
scope.push("Scope", ScriptScope);
scope.push("Cardinality", ScriptCardinality);
scope.push("Direction", ScriptDirection);
scope.push("Merge", ScriptMerge);
scope.push("Operator", ScriptOperator);
scope.push("Barrier", ScriptBarrier);
scope.push("WithOptions", ScriptWithOptions);
scope.push("Pop", ScriptPop);
scope.push("SchemaMode", ScriptSchemaMode);
scope.push("first", Pop::First);
scope.push("last", Pop::Last);
scope.push("all", Pop::All);
scope.push("mixed", Pop::Mixed);
scope
}
pub fn eval(graph: impl IntoAnyGraph, script: &str) -> Result<String, Box<rhai::EvalAltResult>> {
let value = eval_value(graph, script)?;
Ok(render_value(value, VisualizeFormat::Auto, &RenderOptions::unlimited()).text)
}
pub fn eval_value(
graph: impl IntoAnyGraph,
script: &str,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
eval_value_with_limits(
graph,
script,
Default::default(),
&ScriptLimits::default(),
Arc::new(crate::graph::AllowAll),
)
}
pub fn map_eval_error(
err: Box<rhai::EvalAltResult>,
limits: &ScriptLimits,
) -> Box<rhai::EvalAltResult> {
use super::register::display_type_names_in as names;
if let Some(limit) = resource_limit_from_rhai(&err, limits) {
return Box::new(EvalAltResult::ErrorRuntime(
Dynamic::from(crate::error::format_diagnostic(&limit)),
err.position(),
));
}
Box::new(match *err {
EvalAltResult::ErrorFunctionNotFound(signature, position) => {
EvalAltResult::ErrorFunctionNotFound(names(&signature), position)
}
EvalAltResult::ErrorMismatchDataType(expected, actual, position) => {
EvalAltResult::ErrorMismatchDataType(names(&expected), names(&actual), position)
}
EvalAltResult::ErrorMismatchOutputType(expected, actual, position) => {
EvalAltResult::ErrorMismatchOutputType(names(&expected), names(&actual), position)
}
EvalAltResult::ErrorIndexingType(type_name, position) => {
EvalAltResult::ErrorIndexingType(names(&type_name), position)
}
EvalAltResult::ErrorInFunctionCall(name, source, inner, position) => {
EvalAltResult::ErrorInFunctionCall(
name,
source,
map_eval_error(inner, limits),
position,
)
}
other => other,
})
}
pub fn eval_value_with_limits(
graph: impl IntoAnyGraph,
script: &str,
params: std::collections::HashMap<String, ElementProperty>,
limits: &ScriptLimits,
authorizer: Arc<dyn crate::graph::Authorizer>,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
let engine = engine_with_limits(limits, authorizer.clone());
let mut scope = graph_scope_with_options(graph, &[], authorizer);
push_named_params(&mut scope, params, limits.effective_max_value_depth())?;
engine
.eval_with_scope::<Dynamic>(&mut scope, script)
.map_err(|err| map_eval_error(err, limits))
}
pub fn eval_value_atomic(
graph: impl IntoAnyGraph,
script: &str,
params: std::collections::HashMap<String, ElementProperty>,
limits: &ScriptLimits,
authorizer: Arc<dyn crate::graph::Authorizer>,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
let shared = graph.into_any_graph();
let mut params = Some(params);
let mut authorizer = Some(authorizer);
let mut body = |private: &Arc<dyn AnyGraph>| {
let (Some(params), Some(authorizer)) = (params.take(), authorizer.take()) else {
return crate::script::plan::UnitBody(Err(body_ran_twice()));
};
crate::script::plan::UnitBody(
eval_in_unit(private, script, params, limits, authorizer)
.map(|value| retarget_graph(value, private, &shared)),
)
};
shared
.run_unit(crate::script::plan::UnitKind::Atomic, &mut body)
.0
.map(|(value, _)| value)
}
pub fn eval_value_dry_run(
graph: impl IntoAnyGraph,
script: &str,
params: std::collections::HashMap<String, ElementProperty>,
limits: &ScriptLimits,
authorizer: Arc<dyn crate::graph::Authorizer>,
) -> Result<(Dynamic, crate::graph::ChangeSet), Box<rhai::EvalAltResult>> {
let shared = graph.into_any_graph();
let mut params = Some(params);
let mut authorizer = Some(authorizer);
let mut body = |private: &Arc<dyn AnyGraph>| {
let (Some(params), Some(authorizer)) = (params.take(), authorizer.take()) else {
return crate::script::plan::UnitBody(Err(body_ran_twice()));
};
crate::script::plan::UnitBody(
eval_in_unit(private, script, params, limits, authorizer)
.and_then(|value| materialize_traversals(value, limits.max_value_depth)),
)
};
match shared
.run_unit(crate::script::plan::UnitKind::DryRun, &mut body)
.0?
{
(value, Some(changes)) => Ok((value, changes)),
(_, None) => Err(script_error(Box::new(
crate::TraverserError::NotImplemented {
feature: "a dry run without its change set",
},
))),
}
}
fn body_ran_twice() -> Box<rhai::EvalAltResult> {
script_error(Box::new(crate::TraverserError::NotImplemented {
feature: "a whole-script unit whose body runs twice",
}))
}
fn eval_in_unit(
private: &Arc<dyn AnyGraph>,
script: &str,
params: std::collections::HashMap<String, ElementProperty>,
limits: &ScriptLimits,
authorizer: Arc<dyn crate::graph::Authorizer>,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
let engine = engine_with_limits(limits, authorizer.clone());
let mut scope = graph_scope_with_options(private.clone(), &[], authorizer);
let max_depth = limits.effective_max_value_depth();
let evaluated = push_named_params(&mut scope, params, max_depth).and_then(|()| {
engine
.eval_with_scope::<Dynamic>(&mut scope, script)
.map_err(|err| map_eval_error(err, limits))
})?;
if dynamic_exceeds_depth(&evaluated, max_depth) {
return Err(script_error(Box::new(value_too_deep(max_depth))));
}
match returned_closure(&evaluated, "the result") {
Some(location) => Err(script_error_from_script(
crate::error::ScriptError::ReturnedClosure { location },
)),
None => Ok(evaluated),
}
}
pub fn materialize_traversals(
value: Dynamic,
max_depth: usize,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
let max = if max_depth == 0 {
crate::value::depth::DEFAULT_MAX_VALUE_DEPTH
} else {
max_depth
};
if dynamic_exceeds_depth(&value, max) {
return Err(script_error(Box::new(value_too_deep(max))));
}
materialize_checked(value)
}
fn materialize_checked(mut value: Dynamic) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
if value.is::<ScriptGraphTraversalSource>() {
let mut source = value.cast::<ScriptGraphTraversalSource>();
return source.to_list().map(Dynamic::from_array);
}
if let Ok(mut items) = value.as_array_mut() {
for item in items.iter_mut() {
*item = materialize_checked(std::mem::take(item))?;
}
} else if let Ok(mut map) = value.as_map_mut() {
for item in map.values_mut() {
*item = materialize_checked(std::mem::take(item))?;
}
}
Ok(value)
}
fn returned_closure(value: &Dynamic, location: &str) -> Option<String> {
if value.is::<rhai::FnPtr>() {
return Some(location.to_string());
}
let base = if location == "the result" {
"result"
} else {
location
};
if let Some(items) = value.read_lock::<rhai::Array>() {
return items
.iter()
.enumerate()
.find_map(|(index, item)| returned_closure(item, &format!("{base}[{index}]")));
}
if let Some(map) = value.read_lock::<rhai::Map>() {
return map
.iter()
.find_map(|(key, item)| returned_closure(item, &format!("{base}.{key}")));
}
None
}
fn retarget_graph(
mut value: Dynamic,
private: &Arc<dyn AnyGraph>,
shared: &Arc<dyn AnyGraph>,
) -> Dynamic {
if let Some(mut source) = value.write_lock::<ScriptGraphTraversalSource>() {
if source
.g
.as_ref()
.is_some_and(|g| super::plan::same_graph(g, private))
{
source.g = Some(shared.clone());
}
} else if let Ok(mut items) = value.as_array_mut() {
for item in items.iter_mut() {
*item = retarget_graph(std::mem::take(item), private, shared);
}
} else if let Ok(mut map) = value.as_map_mut() {
for item in map.values_mut() {
*item = retarget_graph(std::mem::take(item), private, shared);
}
}
value
}
pub(super) use crate::graph::definition::RESERVED_PARAM_NAMES;
pub fn eval_with_params(
graph: impl IntoAnyGraph,
script: &str,
params: std::collections::HashMap<String, ElementProperty>,
) -> Result<String, Box<rhai::EvalAltResult>> {
let value = eval_value_with_params(graph, script, params)?;
Ok(render_value(value, VisualizeFormat::Auto, &RenderOptions::unlimited()).text)
}
pub fn eval_value_with_params(
graph: impl IntoAnyGraph,
script: &str,
params: std::collections::HashMap<String, ElementProperty>,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
eval_value_with_limits(
graph,
script,
params,
&ScriptLimits::default(),
Arc::new(crate::graph::AllowAll),
)
}
pub fn eval_with_positional_params(
graph: impl IntoAnyGraph,
script: &str,
params: Vec<ElementProperty>,
) -> Result<String, Box<rhai::EvalAltResult>> {
let value = eval_value_with_positional_params(graph, script, params)?;
Ok(render_value(value, VisualizeFormat::Auto, &RenderOptions::unlimited()).text)
}
pub fn eval_value_with_positional_params(
graph: impl IntoAnyGraph,
script: &str,
params: Vec<ElementProperty>,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
let (script, referenced) = rewrite_positional_placeholders(script);
validate_positional_indices(&referenced, params.len())?;
let limits = ScriptLimits::default();
let engine = engine();
let mut scope = graph_scope(graph);
push_positional_params(&mut scope, params, limits.effective_max_value_depth())?;
engine
.eval_with_scope::<Dynamic>(&mut scope, &script)
.map_err(|err| map_eval_error(err, &limits))
}
fn push_named_params(
scope: &mut Scope<'_>,
params: std::collections::HashMap<String, ElementProperty>,
max_depth: usize,
) -> Result<(), Box<rhai::EvalAltResult>> {
let mut entries: Vec<(String, ElementProperty)> = params.into_iter().collect();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut map = rhai::Map::new();
for (name, value) in entries {
validate_param_name(&name)?;
check_param_depth(&value, max_depth)?;
let value = element_property_to_dynamic(value);
map.insert(name.as_str().into(), value.clone());
scope.push_dynamic(name, value);
}
scope.push_dynamic("params", Dynamic::from_map(map));
Ok(())
}
fn push_positional_params(
scope: &mut Scope<'_>,
params: Vec<ElementProperty>,
max_depth: usize,
) -> Result<(), Box<rhai::EvalAltResult>> {
for value in ¶ms {
check_param_depth(value, max_depth)?;
}
let values: Vec<Dynamic> = params
.into_iter()
.map(element_property_to_dynamic)
.collect();
for (offset, value) in values.iter().enumerate() {
scope.push_dynamic(positional_param_name(offset + 1), value.clone());
}
scope.push_dynamic("params", Dynamic::from_array(values));
Ok(())
}
fn check_param_depth(
value: &ElementProperty,
max_depth: usize,
) -> Result<(), Box<rhai::EvalAltResult>> {
if crate::value::depth::property_exceeds_depth(value, max_depth) {
return Err(script_error(Box::new(value_too_deep(max_depth))));
}
Ok(())
}
fn validate_param_name(name: &str) -> Result<(), Box<rhai::EvalAltResult>> {
if RESERVED_PARAM_NAMES.contains(&name) {
return Err(param_error(
name,
format!("'{name}' is reserved by the script scope for the Gremlin DSL"),
));
}
let mut chars = name.chars();
let valid = match chars.next() {
Some(first) if first.is_ascii_alphabetic() || first == '_' => {
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
_ => false,
};
if valid {
Ok(())
} else {
Err(param_error(
name,
"a named parameter must be an identifier: ASCII letters, digits and underscores, \
not starting with a digit"
.to_string(),
))
}
}
fn validate_positional_indices(
referenced: &[usize],
supplied: usize,
) -> Result<(), Box<rhai::EvalAltResult>> {
for index in referenced {
if *index == 0 {
return Err(param_error(
"$0",
"positional parameters are 1-based, so the first placeholder is `$1`".to_string(),
));
}
if *index > supplied {
return Err(param_error(
&format!("${index}"),
format!(
"the script references `${index}` but only {supplied} parameter(s) were supplied"
),
));
}
}
Ok(())
}
fn param_error(parameter: &str, message: String) -> Box<rhai::EvalAltResult> {
Box::new(rhai::EvalAltResult::ErrorRuntime(
Dynamic::from(crate::error::format_diagnostic(
&crate::error::ScriptError::InvalidParameter {
parameter: parameter.to_string(),
message,
},
)),
rhai::Position::NONE,
))
}
fn positional_param_name(index: usize) -> String {
format!("__param_{index}")
}
fn rewrite_positional_placeholders(script: &str) -> (String, Vec<usize>) {
#[derive(Clone, Copy)]
enum Mode {
Code,
LineComment,
BlockComment,
DoubleString,
TemplateString,
CharLiteral,
}
let chars: Vec<char> = script.chars().collect();
let mut rewritten = String::with_capacity(script.len());
let mut referenced: Vec<usize> = Vec::new();
let mut mode = Mode::Code;
let mut depth = 0usize;
let mut cursor = 0usize;
while cursor < chars.len() {
let current = chars[cursor];
let next = chars.get(cursor + 1).copied();
match mode {
Mode::Code => match current {
'/' if next == Some('/') => {
mode = Mode::LineComment;
rewritten.push(current);
}
'/' if next == Some('*') => {
mode = Mode::BlockComment;
depth = 1;
rewritten.push(current);
}
'"' => {
mode = Mode::DoubleString;
rewritten.push(current);
}
'`' => {
mode = Mode::TemplateString;
rewritten.push(current);
}
'\'' => {
mode = Mode::CharLiteral;
rewritten.push(current);
}
'$' if next.is_some_and(|c| c.is_ascii_digit()) => {
let start = cursor + 1;
let mut end = start;
while end < chars.len() && chars[end].is_ascii_digit() {
end += 1;
}
let digits: String = chars[start..end].iter().collect();
match digits.parse::<usize>() {
Ok(index) => {
if !referenced.contains(&index) {
referenced.push(index);
}
rewritten.push_str(&positional_param_name(index));
}
Err(_) => {
rewritten.push(current);
rewritten.push_str(&digits);
}
}
cursor = end;
continue;
}
_ => rewritten.push(current),
},
Mode::LineComment => {
rewritten.push(current);
if current == '\n' {
mode = Mode::Code;
}
}
Mode::BlockComment => {
rewritten.push(current);
if current == '/' && next == Some('*') {
depth += 1;
} else if current == '*' && next == Some('/') {
depth -= 1;
rewritten.push('/');
cursor += 2;
if depth == 0 {
mode = Mode::Code;
}
continue;
}
}
Mode::DoubleString | Mode::CharLiteral => {
rewritten.push(current);
if current == '\\' {
if let Some(escaped) = next {
rewritten.push(escaped);
cursor += 2;
continue;
}
} else if (matches!(mode, Mode::DoubleString) && current == '"')
|| (matches!(mode, Mode::CharLiteral) && current == '\'')
{
mode = Mode::Code;
}
}
Mode::TemplateString => {
rewritten.push(current);
if current == '`' {
mode = Mode::Code;
}
}
}
cursor += 1;
}
(rewritten, referenced)
}