use super::*;
pub fn render_value(
value: Dynamic,
fallback: VisualizeFormat,
options: &RenderOptions,
) -> RenderedOutput {
try_render_value(value, fallback, options).unwrap_or_else(RenderedOutput::complete)
}
pub fn schema_of_value(value: &Dynamic) -> Option<GraphSchema> {
if !super::dynamic::is_schema_map(value) {
return None;
}
let property = super::dynamic::script_value_to_property(
value.clone(),
crate::value::depth::DEFAULT_MAX_VALUE_DEPTH,
)
.ok()?;
GraphSchema::from_value(&property).ok()
}
pub fn try_render_value(
value: Dynamic,
fallback: VisualizeFormat,
options: &RenderOptions,
) -> Result<RenderedOutput, String> {
let value = match value.try_cast_result::<ScriptGraphTraversalSource>() {
Ok(traversal) if traversal.anonymous => {
let text = crate::value::literal::TokenLiteral::token_literal(&traversal);
return Ok(RenderedOutput::complete(
crate::traversal::spelling::respell(&text, options.spelling).into_owned(),
));
}
Ok(traversal) if traversal.is_graph_value() => {
let format = traversal.configured_visualizer().unwrap_or(fallback);
let options = traversal
.session_render_options(options)
.map_err(|err| crate::error::format_diagnostic(&err))?;
let listing = traversal
.graph_listing(options.max_items.map(|n| n.saturating_add(1)))
.map_err(|err| crate::error::format_diagnostic(&err))?;
return crate::traversal::source::render_elements(
&[listing],
format,
&options,
1,
false,
)
.map_err(|err| crate::error::format_diagnostic(&err));
}
Ok(mut traversal) => {
let format = traversal.configured_visualizer().unwrap_or(fallback);
return traversal
.session_render_options(options)
.and_then(|options| traversal.try_visualize_with_options(format, &options))
.map_err(|err| crate::error::format_diagnostic(&err));
}
Err(value) => value,
};
if let Some(schema) = schema_of_value(&value) {
return Ok(RenderedOutput::complete(schema.visualize(fallback)));
}
let value = match value.try_cast_result::<crate::TraversalMetrics>() {
Ok(metrics) => return Ok(RenderedOutput::complete(metrics.to_string())),
Err(value) => value,
};
let value = match value.try_cast_result::<ScriptExecution>() {
Ok(execution) => return execution.render(fallback, options),
Err(value) => value,
};
let value = match value.try_cast_result::<RenderedText>() {
Ok(rendered) => {
return Ok(RenderedOutput::new(
rendered.text.to_string(),
rendered.truncation,
));
}
Err(value) => value,
};
let limit = crate::traversal::render::ItemLimit::new(options.max_items);
let (rows, more_rows) = (options.max_rows, std::cell::Cell::new(false));
let keep = |len: usize| match rows {
Some(n) if len > n => {
more_rows.set(true);
n
}
_ => len,
};
let (text, rows_shown) = if value.is_unit() {
(String::new(), 0)
} else if value.is_array() {
let items = value.cast::<rhai::Array>();
let shown = keep(items.len());
let text = items
.iter()
.take(shown)
.map(|item| spelled_debug_value(item, &limit, options.spelling))
.collect::<Vec<String>>()
.join("\n");
(text, shown)
} else if value.is_string() {
let text = value.into_immutable_string().unwrap_or_default();
return Ok(RenderedOutput::complete(text.to_string()));
} else if value.is_map() {
let map = value.cast::<rhai::Map>();
let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
keys.sort_unstable();
let shown = keep(keys.len());
let text = keys
.iter()
.take(shown)
.filter_map(|key| {
map.get(*key).map(|value| {
format!(
"{key:?}: {}",
spelled_debug_value(value, &limit, options.spelling)
)
})
})
.collect::<Vec<String>>()
.join("\n");
(text, shown)
} else {
(spelled_debug_value(&value, &limit, options.spelling), 1)
};
Ok(RenderedOutput::new(
text,
Truncation::of(
rows_shown,
more_rows.get(),
limit.was_cut(),
options.max_items,
),
))
}
pub fn debug_value(value: &Dynamic) -> String {
bounded_debug_value(value, &crate::traversal::render::ItemLimit::unlimited())
}
pub(super) fn bounded_debug_value(
value: &Dynamic,
limit: &crate::traversal::render::ItemLimit,
) -> String {
spelled_debug_value(value, limit, crate::traversal::Spelling::Snake)
}
fn spelled_debug_value(
value: &Dynamic,
limit: &crate::traversal::render::ItemLimit,
spelling: crate::traversal::Spelling,
) -> String {
debug_at_depth(
value,
limit,
crate::value::depth::DEFAULT_MAX_VALUE_DEPTH,
spelling,
)
}
pub(crate) fn token_literal(value: &Dynamic) -> Option<String> {
use crate::value::literal::TokenLiteral;
macro_rules! try_tokens {
($($ty:ty),* $(,)?) => {
$(
if let Some(token) = value.read_lock::<$ty>() {
return Some(token.token_literal());
}
)*
};
}
try_tokens!(
TraversalScope,
Column,
GType,
Order,
Pop,
Operator,
Barrier,
crate::value::WithOptions,
Direction,
Merge,
Cardinality,
SchemaMode,
CastPolicy,
ProfileType,
VisualizeFormat,
ArgP,
ArgBy,
Pick,
CardinalityValue,
ScriptSet,
);
super::tokens::for_each_marker!(try_tokens);
if let Some(traversal) = value.read_lock::<ScriptGraphTraversalSource>()
&& traversal.anonymous
{
return Some(traversal.token_literal());
}
None
}
pub(crate) fn value_literal(value: &Dynamic) -> String {
if let Some(token) = token_literal(value) {
return token;
}
match dynamic_to_element_property(value.clone(), crate::value::depth::DEFAULT_MAX_VALUE_DEPTH) {
Ok(property) => crate::value::literal::rhai_literal(&property),
Err(_) => debug_value(value),
}
}
fn debug_at_depth(
value: &Dynamic,
limit: &crate::traversal::render::ItemLimit,
levels: usize,
spelling: crate::traversal::Spelling,
) -> String {
if let Some(uuid) = value.read_lock::<ScriptUuid>() {
return uuid.canonical();
}
if let Some(path) = value.read_lock::<crate::value::JsonPath>() {
return path.to_string();
}
if let Some(map) = value.read_lock::<ScriptMap>() {
return script_map_text(&map, limit);
}
if let Some(token) = token_literal(value) {
return crate::traversal::spelling::respell(&token, spelling).into_owned();
}
if let Some(rendered) = value.read_lock::<RenderedText>() {
return format!("{:?}", rendered.as_str());
}
if value.is_array()
&& let Some(items) = value.read_lock::<rhai::Array>()
{
let Some(inner) = levels.checked_sub(1) else {
return format!("[{}]", crate::traversal::render::CUT_MARKER);
};
let keep = limit.keep(items.len());
let mut rendered: Vec<String> = items
.iter()
.take(keep)
.map(|item| debug_at_depth(item, limit, inner, spelling))
.collect();
if keep < items.len() {
rendered.push(crate::traversal::render::CUT_MARKER.to_string());
}
return format!("[{}]", rendered.join(", "));
}
if value.is_map()
&& let Some(map) = value.read_lock::<rhai::Map>()
{
let Some(inner) = levels.checked_sub(1) else {
return format!("#{{{}}}", crate::traversal::render::CUT_MARKER);
};
let keep = limit.keep(map.len());
let mut entries: Vec<String> = map
.iter()
.take(keep)
.map(|(key, value)| {
format!(
"{:?}: {}",
key.as_str(),
debug_at_depth(value, limit, inner, spelling)
)
})
.collect();
if keep < map.len() {
entries.push(crate::traversal::render::CUT_MARKER.to_string());
}
return format!("#{{{}}}", entries.join(", "));
}
format!("{value:?}")
}
fn invalid_render_option(
key: &str,
reason: impl Into<String>,
) -> Box<crate::error::TraverserError> {
Box::new(crate::error::TraverserError::InvalidOption {
key: key.to_string(),
reason: reason.into(),
})
}
pub(super) fn render_options_from(
options_map: &std::collections::HashMap<CompactString, ElementProperty>,
base: &RenderOptions,
) -> crate::error::TraverserResult<RenderOptions> {
let mut options = base.clone();
if let Some(value) = options_map.get(RENDER_MAX_ROWS) {
options.max_rows = Some(positive_render_option(RENDER_MAX_ROWS, value)?);
}
if let Some(value) = options_map.get(RENDER_MAX_ITEMS) {
options.max_items = Some(positive_render_option(RENDER_MAX_ITEMS, value)?);
}
Ok(options)
}
fn positive_render_option(
key: &str,
value: &ElementProperty,
) -> crate::error::TraverserResult<usize> {
match value {
ElementProperty::Number(n) => match n.as_i64() {
Some(v) if v >= 1 => Ok(usize::try_from(v).unwrap_or(usize::MAX)),
_ => Err(invalid_render_option(
key,
format!("expected a positive integer, got {n}"),
)),
},
other => Err(invalid_render_option(
key,
format!(
"expected a positive integer, got {}",
crate::value::physical::to_json_text(other).unwrap_or_default()
),
)),
}
}
fn call_limit(key: &str, value: &Dynamic) -> crate::error::TraverserResult<usize> {
match value.as_int() {
Ok(v) if v >= 1 => Ok(usize::try_from(v).unwrap_or(usize::MAX)),
Ok(v) => Err(invalid_render_option(
key,
format!(
"{v} is not a positive number of {}",
if key.contains("row") { "rows" } else { "items" }
),
)),
Err(type_name) => Err(invalid_render_option(
key,
format!(
"expected a positive integer, got {}",
super::register::display_type_name(type_name)
),
)),
}
}
pub(super) fn dynamic_to_render_options(
arg: &Dynamic,
session: &RenderOptions,
) -> crate::error::TraverserResult<RenderOptions> {
let mut options = RenderOptions {
max_rows: session.max_rows,
max_items: session.max_items,
..RenderOptions::default()
};
if arg.is_array() {
let arr = arg.clone().cast::<rhai::Array>();
let cols: Vec<String> = arr.into_iter().map(dynamic_to_string).collect();
options.columns = Some(cols);
return Ok(options);
}
if arg.is_map() {
let map = arg.clone().cast::<rhai::Map>();
let mut max_rows = None;
let mut max_items = None;
let mut no_limit = false;
for (key, val) in map {
match key.as_str() {
"columns" => {
if val.is_array() {
let arr = val.cast::<rhai::Array>();
let cols: Vec<String> = arr.into_iter().map(dynamic_to_string).collect();
options.columns = Some(cols);
}
}
"max_width" | "maxWidth" => {
if let Ok(w) = val.as_int()
&& w >= 0
{
options.max_width = Some(w as usize);
}
}
"truncate" => {
if let Ok(t) = val.as_int()
&& t >= 0
{
options.truncate = Some(t as usize);
}
}
"row_numbers" | "rowNumbers" => {
if let Ok(b) = val.as_bool() {
options.row_numbers = b;
}
}
"max_rows" | "maxRows" => max_rows = Some(call_limit("max_rows", &val)?),
"max_items" | "maxItems" => max_items = Some(call_limit("max_items", &val)?),
"no_limit" | "noLimit" => {
no_limit = val.as_bool().map_err(|type_name| {
invalid_render_option(
"no_limit",
format!(
"expected true or false, got {}",
super::register::display_type_name(type_name)
),
)
})?;
}
_ => {}
}
}
if no_limit {
if max_rows.is_some() || max_items.is_some() {
return Err(invalid_render_option(
"no_limit",
"no_limit: true removes every display limit and cannot be combined with max_rows or max_items",
));
}
options.max_rows = None;
options.max_items = None;
}
if max_rows.is_some() {
options.max_rows = max_rows;
}
if max_items.is_some() {
options.max_items = max_items;
}
return Ok(options);
}
Err(invalid_render_option(
"options",
format!(
"expected an array of columns or an options map, got {}",
type_name_of(arg)
),
))
}
pub const RENDER_MAX_ROWS: &str = "render.max_rows";
pub const RENDER_MAX_ITEMS: &str = "render.max_items";
pub const RENDER_SPELLING: &str = crate::traversal::context::RENDER_SPELLING;
pub fn render_scope_options(options: &RenderOptions) -> Vec<(&'static str, ElementProperty)> {
let as_prop = |n: usize| ElementProperty::from(i64::try_from(n).unwrap_or(i64::MAX));
let mut entries = Vec::with_capacity(2);
if let Some(n) = options.max_rows {
entries.push((RENDER_MAX_ROWS, as_prop(n)));
}
if let Some(n) = options.max_items {
entries.push((RENDER_MAX_ITEMS, as_prop(n)));
}
if options.spelling != crate::traversal::Spelling::Snake {
entries.push((
RENDER_SPELLING,
ElementProperty::from(options.spelling.as_str()),
));
}
entries
}
pub fn set_scope_render_options(scope: &mut Scope, options: &RenderOptions) {
for name in ["g", "_g"] {
if let Some(value) = scope.get_mut(name)
&& let Some(mut source) = value.write_lock::<ScriptGraphTraversalSource>()
{
source.options.remove(RENDER_MAX_ROWS);
source.options.remove(RENDER_MAX_ITEMS);
source.options.remove(RENDER_SPELLING);
for (key, value) in render_scope_options(options) {
source.options.insert(CompactString::new(key), value);
}
}
}
}
#[derive(Debug, Clone)]
pub struct RenderedText {
pub(super) text: ImmutableString,
truncation: Option<Truncation>,
}
impl RenderedText {
pub(super) fn new(rendered: RenderedOutput) -> Self {
Self {
text: ImmutableString::from(rendered.text),
truncation: rendered.truncation,
}
}
pub(super) fn diagnostic(text: ImmutableString) -> Self {
Self {
text,
truncation: None,
}
}
pub(super) fn as_str(&self) -> &str {
&self.text
}
}
impl std::fmt::Display for RenderedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.text)
}
}