use std::collections::HashMap;
use compact_str::CompactString;
use rhai::Dynamic;
use crate::error::{BoxedTraverserError, DiagnosticError, TraverserError};
use crate::graph::schema::VisualizeFormat;
use crate::traversal::render::{RenderOptions, RenderedOutput};
use crate::traversal::source::ListItem;
use crate::value::{ElementProperty, PropertyMap};
use crate::{ExecOptions, ProfileType, StepLocation, TraversalMetrics};
const EXECUTE_OPTION_KEYS: &str = "profile, profile_types";
#[derive(Debug, Clone)]
struct ExecutionErrorData {
message: String,
help: Option<String>,
diagnostic: String,
location: Option<StepLocation>,
}
impl ExecutionErrorData {
fn of(err: &TraverserError) -> Self {
let root = err.root_cause();
let message = if std::ptr::eq(root, err) {
err.to_string()
} else {
format!("{err}: {root}")
};
Self {
message,
help: err.help(),
diagnostic: crate::error::format_diagnostic(err),
location: err.step_location(),
}
}
fn to_property(&self) -> ElementProperty {
let mut map = PropertyMap::default();
map.insert("message", text(&self.message));
if let Some(help) = &self.help {
map.insert("help", text(help));
}
if let Some(location) = &self.location {
map.insert("step_id", text(&location.step_id));
map.insert(
"step_path",
ElementProperty::Array(
location
.step_path
.iter()
.map(|entry| {
let mut step = PropertyMap::default();
step.insert("id", text(&entry.id));
step.insert("name", text(&entry.name));
ElementProperty::Object(Box::new(step))
})
.collect(),
),
);
}
map.insert("diagnostic", text(&self.diagnostic));
ElementProperty::Object(Box::new(map))
}
}
fn text(value: &str) -> ElementProperty {
ElementProperty::String(CompactString::new(value))
}
#[derive(Clone)]
pub struct ScriptExecution {
items: Vec<ListItem>,
error: Option<ExecutionErrorData>,
profile: Option<TraversalMetrics>,
mutated: bool,
changes: crate::graph::UnitChanges,
options: HashMap<CompactString, ElementProperty>,
origin: super::access::GraphOrigin,
}
impl ScriptExecution {
pub(crate) fn new(
items: Vec<ListItem>,
error: Option<BoxedTraverserError>,
profile: Option<TraversalMetrics>,
options: HashMap<CompactString, ElementProperty>,
origin: super::access::GraphOrigin,
) -> Self {
Self {
items,
error: error.as_deref().map(ExecutionErrorData::of),
profile,
mutated: false,
changes: crate::graph::UnitChanges::NONE,
options,
origin,
}
}
pub(crate) fn with_mutated(
mut self,
mutated: bool,
changes: crate::graph::UnitChanges,
) -> Self {
self.mutated = mutated;
self.changes = changes;
self
}
pub fn changes(&mut self) -> Dynamic {
let mut map = rhai::Map::new();
map.insert("data".into(), Dynamic::from_bool(self.changes.data));
map.insert("schema".into(), Dynamic::from_bool(self.changes.schema));
map.insert(
"definitions".into(),
Dynamic::from_bool(self.changes.definitions),
);
Dynamic::from_map(map)
}
pub fn mutated(&mut self) -> bool {
self.mutated
}
pub fn results(&mut self) -> rhai::Array {
self.items
.iter()
.cloned()
.map(|item| super::list_item_to_dynamic(item, &self.origin))
.collect()
}
pub fn error(&mut self) -> Dynamic {
self.error
.as_ref()
.map(|err| super::element_property_to_dynamic(err.to_property()))
.unwrap_or(Dynamic::UNIT)
}
pub fn profile(&mut self) -> Dynamic {
self.profile
.clone()
.map(Dynamic::from)
.unwrap_or(Dynamic::UNIT)
}
pub fn is_ok(&mut self) -> bool {
self.error.is_none()
}
fn to_property(&self) -> crate::error::TraverserResult<ElementProperty> {
let mut map = PropertyMap::default();
map.insert(
"results",
ElementProperty::Array(list_items_property(&self.items)?),
);
map.insert(
"error",
self.error
.as_ref()
.map_or(ElementProperty::Null, ExecutionErrorData::to_property),
);
map.insert(
"profile",
self.profile
.as_ref()
.map_or(ElementProperty::Null, TraversalMetrics::to_property),
);
Ok(ElementProperty::Object(Box::new(map)))
}
pub fn to_map(&mut self) -> Dynamic {
let mut map = rhai::Map::new();
map.insert("results".into(), Dynamic::from(self.results()));
map.insert("error".into(), self.error());
map.insert(
"profile".into(),
self.profile
.as_ref()
.map(|p| super::element_property_to_dynamic(p.to_property()))
.unwrap_or(Dynamic::UNIT),
);
map.insert("mutated".into(), Dynamic::from_bool(self.mutated));
map.insert("changes".into(), self.changes());
Dynamic::from_map(map)
}
pub fn to_json(&mut self) -> Result<String, Box<rhai::EvalAltResult>> {
let value = self.to_property().map_err(super::script_error)?;
Ok(crate::value::physical::to_json_text(&value).unwrap_or_default())
}
pub(crate) fn render(
&self,
fallback: VisualizeFormat,
base: &RenderOptions,
) -> Result<RenderedOutput, String> {
if let Some(err) = &self.error {
let mut text = err.diagnostic.clone();
if let Some(profile) = &self.profile {
text.push_str("\n\n");
text.push_str(&profile.to_string());
}
return Err(text);
}
let mut rendered = self.render_results(fallback, base)?;
if let Some(profile) = &self.profile {
if !rendered.text.is_empty() {
rendered.text.push_str("\n\n");
}
rendered.text.push_str(&profile.to_string());
}
Ok(rendered)
}
pub fn render_results(
&self,
fallback: VisualizeFormat,
base: &RenderOptions,
) -> Result<RenderedOutput, String> {
if let Some(err) = &self.error {
return Err(err.diagnostic.clone());
}
let format = self
.options
.get("visualizer")
.and_then(|value| value.as_str())
.and_then(VisualizeFormat::from_name)
.unwrap_or(fallback);
let options = super::render_options_from(&self.options, base)
.map_err(|err| crate::error::format_diagnostic(&err))?;
let mut elements = list_items_property(&self.items)
.map_err(|err| crate::error::format_diagnostic(&err))?;
let more_rows = match options.max_rows {
Some(n) if elements.len() > n => {
elements.truncate(n);
true
}
_ => false,
};
let rows_shown = elements.len();
crate::traversal::source::render_elements(
&elements, format, &options, rows_shown, more_rows,
)
.map_err(|err| crate::error::format_diagnostic(&err))
}
}
fn list_items_property(items: &[ListItem]) -> crate::error::TraverserResult<Vec<ElementProperty>> {
items
.iter()
.map(|item| match item {
ListItem::Value(value) => Ok(value.clone()),
ListItem::Graph(graph) => crate::graph::subgraph::graph_to_property(graph),
})
.collect()
}
pub(crate) fn parse_execute_options(options: Dynamic) -> Result<ExecOptions, BoxedTraverserError> {
let invalid = |key: &str, reason: String| {
Box::new(TraverserError::InvalidOption {
key: format!("execute.{key}"),
reason,
})
};
let Some(map) = options.read_lock::<rhai::Map>() else {
return Err(invalid(
"options",
format!(
"expected a map such as #{{profile: true}}, got {}",
super::type_name_of(&options)
),
));
};
let mut profile: Option<bool> = None;
let mut types = ProfileType::none();
let mut types_given = false;
for (key, value) in map.iter() {
match key.as_str() {
"profile" => match value.as_bool() {
Ok(flag) => profile = Some(flag),
Err(actual) => {
return Err(invalid(
"profile",
format!(
"expected true or false, got {}",
super::register::display_type_name(actual)
),
));
}
},
"profile_types" => {
let Some(list) = value.read_lock::<rhai::Array>() else {
return Err(invalid(
"profile_types",
format!(
"expected a list such as [ProfileType.Memory], got {}",
super::type_name_of(value)
),
));
};
for item in list.iter() {
types |= profile_type_of(item).ok_or_else(|| {
invalid(
"profile_types",
format!(
"unknown profile type {}; known: ProfileType.Memory (\"memory\")",
super::debug_value(item)
),
)
})?;
}
types_given = true;
}
other => {
return Err(invalid(
other,
format!("unknown option; execute() accepts {EXECUTE_OPTION_KEYS}"),
));
}
}
}
match (profile, types_given) {
(Some(false), true) => Err(invalid(
"profile_types",
"profile types need profiling: drop `profile: false` or the types".to_string(),
)),
(Some(true), _) | (None, true) => Ok(ExecOptions::profile(types)),
(Some(false), false) | (None, false) => Ok(ExecOptions::new()),
}
}
fn profile_type_of(value: &Dynamic) -> Option<ProfileType> {
if let Some(flags) = value.read_lock::<ProfileType>() {
return Some(*flags);
}
let name = value.read_lock::<rhai::ImmutableString>()?;
match name.as_str() {
"memory" | "Memory" => Some(ProfileType::Memory),
_ => None,
}
}
pub(crate) fn register(registry: &mut super::register::Registry) {
registry
.register_type_with_name::<TraversalMetrics>("TraversalMetrics")
.register_get("duration_ns", |m: &mut TraversalMetrics| {
i64::try_from(m.duration().as_nanos()).unwrap_or(i64::MAX)
})
.register_get("metrics", |m: &mut TraversalMetrics| {
metrics_field(m, "metrics")
})
.register_get("optimizer_rules_applied", |m: &mut TraversalMetrics| {
m.optimizer_rules_applied()
.iter()
.map(|rule| Dynamic::from(rule.clone()))
.collect::<rhai::Array>()
})
.register_fn_as(&["to_map", "toMap"], metrics_to_map)
.register_fn_as(&["to_json", "toJson"], |m: &mut TraversalMetrics| {
m.to_json()
})
.register_fn_as(
&["to_table", "toTable", "to_string", "to_debug"],
|m: &mut TraversalMetrics| m.to_string(),
);
registry
.register_type_with_name::<ScriptExecution>("Execution")
.register_get("results", ScriptExecution::results)
.register_get("error", ScriptExecution::error)
.register_get("profile", ScriptExecution::profile)
.register_get("mutated", ScriptExecution::mutated)
.register_get("changes", ScriptExecution::changes)
.register_get("is_ok", ScriptExecution::is_ok)
.register_get("isOk", ScriptExecution::is_ok)
.register_fn_as(&["to_map", "toMap"], ScriptExecution::to_map)
.register_fn_as(&["to_json", "toJson"], ScriptExecution::to_json)
.register_fn("to_string", |e: &mut ScriptExecution| {
e.render(VisualizeFormat::Auto, &RenderOptions::default())
.map(|rendered| rendered.text)
.unwrap_or_else(|diagnostic| diagnostic)
})
.register_fn("to_debug", |e: &mut ScriptExecution| {
e.to_json().unwrap_or_else(|err| err.to_string())
});
}
pub fn metrics_to_map(metrics: &mut TraversalMetrics) -> Dynamic {
super::element_property_to_dynamic(metrics.to_property())
}
fn metrics_field(metrics: &TraversalMetrics, key: &str) -> Dynamic {
match metrics.to_property() {
ElementProperty::Object(map) => map
.get(key)
.cloned()
.map(super::element_property_to_dynamic)
.unwrap_or(Dynamic::UNIT),
_ => Dynamic::UNIT,
}
}