use crate::cli::parser::{Command as CliCommand, CommonArgsAll};
use crate::cli::wrappers::resolve_backend;
use crate::commands::{CommandContext, CommandFactory, extract_common};
use crate::shell::completer::ShellHelper;
use anyhow::{Context, Result, anyhow};
use clap::{CommandFactory as ClapCommandFactory, Parser};
use prefixmap::PrefixMap;
use rudof_lib::formats::{BackendSpec, InputSpec};
use rudof_lib::{DataStats, RudofConfig, TomlConfig};
use rustyline::error::ReadlineError;
use rustyline::history::DefaultHistory;
use rustyline::{Cmd, CompletionType, Config, Editor, EventHandler, KeyEvent};
use std::io;
use std::io::Write as _;
use std::process::Command;
pub(super) const PROMPT: &str = "rudof> ";
pub(super) const CONTINUATION_PROMPT: &str = " ... ";
const BANNER: &str = concat!(
" ",
"\x1b[34m_\x1b[0m",
" ",
"\x1b[34m___\x1b[0m\n",
" ",
"\x1b[34m| |\x1b[0m",
" ",
"\x1b[34m/ __)\x1b[0m\n",
" ",
"\x1b[34m____\x1b[0m",
" ",
"\x1b[90m_ _\x1b[0m",
" ",
"\x1b[34m_ | | \x1b[0m",
"\x1b[90m___ \x1b[0m",
"\x1b[34m| |__\x1b[0m\n",
" ",
"\x1b[34m/ ___)\x1b[0m",
"\x1b[90m | | |\x1b[0m",
"\x1b[34m/ || |\x1b[0m",
"\x1b[90m/ _ \\\x1b[0m",
"\x1b[34m| __)\x1b[0m\n",
"\x1b[34m| |\x1b[0m ",
"\x1b[90m| |_| \x1b[0m",
"\x1b[34m( (_| |\x1b[0m",
"\x1b[90m |_| \x1b[0m",
"\x1b[34m| |\x1b[0m\n",
"\x1b[34m|_| \x1b[0m",
"\x1b[90m\\____|\x1b[0m",
"\x1b[34m\\____|\x1b[0m",
"\x1b[90m\\___/\x1b[0m",
"\x1b[34m|_|\x1b[0m",
);
#[derive(Parser)]
#[command(name = "rudof", about = "rudof interactive shell commands", no_binary_name = true)]
struct ReplLine {
#[command(subcommand)]
command: CliCommand,
}
fn read_command(editor: &mut Editor<ShellHelper, DefaultHistory>) -> Result<String, ReadlineError> {
let mut buffer = editor.readline(PROMPT)?;
while !buffer.trim().is_empty() && shlex::split(&buffer).is_none() {
if let Some(helper) = editor.helper() {
helper.set_continuation(true);
}
let result = editor.readline(CONTINUATION_PROMPT);
if let Some(helper) = editor.helper() {
helper.set_continuation(false);
}
match result {
Ok(next) => {
buffer.push('\n');
buffer.push_str(&next);
},
Err(err @ ReadlineError::Eof) => {
let _ = writeln!(
std::io::stderr(),
"Warning: input ended with an unterminated quote; discarding: {buffer}"
);
return Err(err);
},
Err(err) => return Err(err),
}
}
Ok(buffer)
}
pub fn run(ctx: &mut CommandContext) -> Result<()> {
let command_names = command_names();
let endpoint_names = registered_endpoint_names(ctx);
let config_keys = config_key_paths(ctx).unwrap_or_default();
let config = Config::builder()
.completion_type(CompletionType::List)
.completion_show_all_if_ambiguous(true)
.check_cursor_position(true)
.build();
let mut editor: Editor<ShellHelper, DefaultHistory> = Editor::with_config(config)?;
editor.set_helper(Some(ShellHelper::new(command_names, endpoint_names, config_keys)));
editor.bind_sequence(KeyEvent::ctrl('J'), EventHandler::Simple(Cmd::Newline));
editor.bind_sequence(KeyEvent::alt('\r'), EventHandler::Simple(Cmd::Newline));
let history_path = history_file();
if let Some(path) = &history_path {
let _ = editor.load_history(path);
}
writeln!(ctx.writer, "{BANNER}")?;
writeln!(ctx.writer, "Type 'help' for available commands, 'exit' to quit.")?;
ctx.writer.flush()?;
if let Err(err) = crate::signals::install_shell_ctrl_c_handler() {
writeln!(
std::io::stderr(),
"Warning: could not install Ctrl-C handler ({err}); Ctrl-C during a running command will exit the shell"
)?;
}
loop {
match read_command(&mut editor) {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Err(err) = editor.add_history_entry(line) {
writeln!(std::io::stderr(), "Warning: could not record history entry: {err}")?;
}
match line {
"exit" | "quit" => break,
"help" | "?" => print_help(ctx)?,
_ => {
rudof_rdf::cancellation::reset();
if let Err(err) = dispatch_catching_panics(line, ctx) {
writeln!(
std::io::stderr(),
"{} {}",
crate::output::error_prefix(),
crate::output::format_error(&err)
)?;
}
},
}
ctx.writer.flush()?;
},
Err(ReadlineError::Interrupted) => continue,
Err(ReadlineError::Eof) => break,
Err(err) => {
writeln!(std::io::stderr(), "Readline error: {err}")?;
break;
},
}
}
if let Some(path) = &history_path
&& let Err(err) = editor.save_history(path)
{
writeln!(std::io::stderr(), "Warning: could not save shell history: {err}")?;
}
Ok(())
}
fn dispatch_catching_panics(line: &str, ctx: &mut CommandContext) -> Result<()> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| dispatch(line, ctx))) {
Ok(result) => result,
Err(payload) => Err(anyhow!("command panicked: {}", panic_message(&payload))),
}
}
fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"no further information available".to_string()
}
}
fn dispatch(line: &str, ctx: &mut CommandContext) -> Result<()> {
if let Some(command_line) = line.strip_prefix('!') {
return run_external_command(command_line.trim(), ctx);
}
let mut tokens = shlex::split(line).ok_or_else(|| anyhow!("unable to parse line (check quoting): {line}"))?;
if tokens.is_empty() {
return Ok(());
}
if tokens[0] == "endpoint" {
return handle_endpoint(&tokens[1..], ctx);
}
if tokens[0] == "reset" {
return handle_reset(&tokens[1..], ctx);
}
if tokens[0] == "prefixes" {
return handle_prefixes(&tokens[1..], ctx);
}
if tokens[0] == "config" && tokens.get(1).is_some_and(|sub| sub == "get" || sub == "set") {
return handle_config(&tokens[1..], ctx);
}
let merge = tokens[0] == "data" && take_merge_flag(&mut tokens);
apply_bare_resource_convenience(&mut tokens);
expand_prefixed_resource_tokens(&mut tokens, ctx);
let parsed = match ReplLine::try_parse_from(&tokens) {
Ok(repl_line) => repl_line.command,
Err(err) => {
writeln!(ctx.writer, "{err}")?;
return Ok(());
},
};
if matches!(parsed, CliCommand::Shell(_)) {
writeln!(ctx.writer, "Already inside the shell.")?;
return Ok(());
}
let should_replace_data =
!merge && matches!(&parsed, CliCommand::Data(args) if has_new_data_source(&args.common, &args.data));
let common = extract_common(&parsed);
let output_path = common.output().cloned().filter(|path| path.to_string_lossy() != "-");
let force_overwrite = common.force_overwrite();
let stats_kind = output_path.is_none().then(|| StatsKind::of(&parsed)).flatten();
let command = CommandFactory::create(parsed)?;
if should_replace_data {
ctx.rudof.reset_data().execute();
}
if let Some(kind) = stats_kind {
let previous_writer = std::mem::replace(&mut ctx.writer, Box::new(io::sink()));
let result = command.execute(ctx);
ctx.writer = previous_writer;
result?;
writeln!(ctx.writer, "{}", kind.stats_line(ctx))?;
return Ok(());
}
let Some(path) = output_path else {
return command.execute(ctx);
};
let (file_writer, _color) = crate::output::get_writer(&Some(path.clone()), force_overwrite)?;
let previous_writer = std::mem::replace(&mut ctx.writer, file_writer);
let result = command.execute(ctx).and_then(|()| Ok(ctx.writer.flush()?));
ctx.writer = previous_writer;
result?;
writeln!(ctx.writer, "Output saved in {}", path.display())?;
Ok(())
}
#[derive(Clone, Copy)]
enum StatsKind {
Data,
Shex,
Shacl,
DCTap,
Pgschema,
Service,
}
impl StatsKind {
fn of(command: &CliCommand) -> Option<Self> {
match command {
CliCommand::Data(args) => has_new_data_source(&args.common, &args.data).then_some(Self::Data),
CliCommand::Shex(args) => args.schema.is_some().then_some(Self::Shex),
CliCommand::Shacl(args) => {
(has_new_data_source(&args.common, &args.data) || args.shapes.is_some()).then_some(Self::Shacl)
},
CliCommand::DCTap(args) => args.file.is_some().then_some(Self::DCTap),
CliCommand::Pgschema(args) => args.schema.is_some().then_some(Self::Pgschema),
CliCommand::Service(args) => args.service.is_some().then_some(Self::Service),
_ => None,
}
}
fn stats_line(self, ctx: &CommandContext) -> String {
match self {
Self::Data => match ctx.rudof.data_stats() {
Some(DataStats::Rdf { triples }) => format!("{triples} triple(s) loaded"),
Some(DataStats::Pg { nodes, edges }) => format!("{nodes} node(s), {edges} edge(s) loaded"),
None => "No data loaded".to_string(),
},
Self::Shex => match ctx.rudof.shex_schema().and_then(|schema| schema.shapes()) {
Some(shapes) => format!("{} shape(s) loaded", shapes.len()),
None => "No shapes loaded".to_string(),
},
Self::Shacl => match ctx.rudof.shacl_shapes() {
Some(shapes) => format!("{} shape(s) loaded", shapes.iter().count()),
None => "No shapes loaded".to_string(),
},
Self::DCTap => match ctx.rudof.dctap() {
Some(dctap) => format!("{} shape(s) loaded", dctap.shapes().count()),
None => "No shapes loaded".to_string(),
},
Self::Pgschema => match ctx.rudof.pg_schema() {
Some(schema) => format!(
"{} node type(s), {} edge type(s) loaded",
schema.node_type_count(),
schema.edge_type_count()
),
None => "No schema loaded".to_string(),
},
Self::Service => match ctx.rudof.service_description() {
Some(service) => format!(
"{} graph(s), {} feature(s) loaded",
service.available_graphs_count(),
service.feature_count()
),
None => "No service description loaded".to_string(),
},
}
}
}
fn has_new_data_source(common: &CommonArgsAll, data: &[InputSpec]) -> bool {
!data.is_empty() || matches!(resolve_backend(common), BackendSpec::Endpoint(_))
}
fn handle_endpoint(args: &[String], ctx: &mut CommandContext) -> Result<()> {
match args {
[] => show_active_endpoint(ctx),
[arg] if arg.to_lowercase().ends_with(".toml") => register_endpoint_from_file(arg, ctx),
[name] => activate_endpoint(name, ctx),
_ => {
writeln!(ctx.writer, "Usage: endpoint [NAME|FILE.toml]")?;
Ok(())
},
}
}
fn register_endpoint_from_file(path: &str, ctx: &mut CommandContext) -> Result<()> {
let mut rdf_data = ctx.rudof.config().execute().rdf_data().clone();
let name = rdf_data
.load_endpoint_description(path)
.map_err(|err| anyhow!("Failed to load endpoint description from '{path}': {err}"))?;
let config = ctx.rudof.config().execute().clone().with_rdf_data(rdf_data);
ctx.rudof.update_config(config).execute();
writeln!(ctx.writer, "Registered endpoint '{name}' from {path}")?;
activate_endpoint(&name, ctx)
}
fn show_active_endpoint(ctx: &mut CommandContext) -> Result<()> {
let Some(name) = ctx.active_endpoint.clone() else {
writeln!(ctx.writer, "No endpoint is currently active.")?;
writeln!(ctx.writer, "Use 'endpoint NAME' to activate one.")?;
writeln!(ctx.writer, "{}", registered_endpoints_line(ctx))?;
return Ok(());
};
match endpoint_url(ctx, &name) {
Some(url) => writeln!(ctx.writer, "Active endpoint: {name} ({url})")?,
None => writeln!(ctx.writer, "Active endpoint: {name}")?,
}
Ok(())
}
fn activate_endpoint(name: &str, ctx: &mut CommandContext) -> Result<()> {
let Some(canonical_name) = canonical_endpoint_name(ctx, name) else {
writeln!(ctx.writer, "Unknown endpoint '{name}'.")?;
writeln!(ctx.writer, "{}", registered_endpoints_line(ctx))?;
return Ok(());
};
ctx.rudof.load_data().with_endpoint(&canonical_name).execute()?;
ctx.active_endpoint = Some(canonical_name.clone());
let url = endpoint_url(ctx, &canonical_name).unwrap_or_default();
writeln!(ctx.writer, "Active endpoint: {canonical_name} ({url})")?;
Ok(())
}
fn canonical_endpoint_name(ctx: &CommandContext, name: &str) -> Option<String> {
ctx.rudof
.config()
.execute()
.rdf_data()
.find_endpoint(name)
.map(|e| e.name().to_string())
}
fn endpoint_url(ctx: &CommandContext, name: &str) -> Option<String> {
ctx.rudof
.config()
.execute()
.rdf_data()
.find_endpoint(name)
.map(|e| e.query_url().to_string())
}
const RESET_TARGETS: &[&str] = &[
"data",
"shex",
"shacl",
"pgschema",
"shapemap",
"dctap",
"service",
"query",
"sparql",
"typemap",
"rdf-config",
"endpoint",
];
fn handle_reset(args: &[String], ctx: &mut CommandContext) -> Result<()> {
match args {
[] => reset_everything(ctx)?,
[only] if only == "all" => reset_everything(ctx)?,
targets => {
let unknown: Vec<&str> = targets
.iter()
.map(String::as_str)
.filter(|target| !RESET_TARGETS.contains(target))
.collect();
if !unknown.is_empty() {
writeln!(ctx.writer, "Unknown reset target(s): {}.", unknown.join(", "))?;
writeln!(ctx.writer, "Valid targets: {}, or 'all'.", RESET_TARGETS.join(", "))?;
return Ok(());
}
for target in targets {
reset_target(target, ctx);
}
writeln!(ctx.writer, "Reset: {}.", targets.join(", "))?;
},
}
Ok(())
}
fn reset_everything(ctx: &mut CommandContext) -> Result<()> {
ctx.rudof.reset_all().execute();
ctx.active_endpoint = None;
writeln!(ctx.writer, "Reset all session state.")?;
Ok(())
}
fn reset_target(target: &str, ctx: &mut CommandContext) {
match target {
"data" => ctx.rudof.reset_data().execute(),
"shex" => ctx.rudof.reset_shex_schema().execute(),
"shacl" => ctx.rudof.reset_shacl_shapes().execute(),
"pgschema" => ctx.rudof.reset_pg_schema().execute(),
"shapemap" => ctx.rudof.reset_shapemap().execute(),
"dctap" => ctx.rudof.reset_dctap().execute(),
"service" => ctx.rudof.reset_service_description().execute(),
"query" => {
ctx.rudof.reset_sparql_query().execute();
ctx.rudof.reset_query_results().execute();
},
"sparql" => ctx.rudof.reset_sparql_query().execute(),
"typemap" => ctx.rudof.reset_typemap().execute(),
"rdf-config" => ctx.rudof.reset_rdf_config().execute(),
"endpoint" => ctx.active_endpoint = None,
_ => unreachable!("target validated against RESET_TARGETS by handle_reset"),
}
}
fn handle_prefixes(args: &[String], ctx: &mut CommandContext) -> Result<()> {
match args {
[] => show_prefixes(ctx),
[add, alias, iri] if add == "add" => {
ctx.rudof.add_prefix(alias, iri).execute()?;
writeln!(ctx.writer, "Added prefix {alias}: <{iri}>")?;
Ok(())
},
[rm, alias] if rm == "rm" => {
ctx.rudof.remove_prefix(alias).execute()?;
writeln!(ctx.writer, "Removed prefix {alias}")?;
Ok(())
},
[rename, old_alias, new_alias] if rename == "rename" => {
ctx.rudof.rename_prefix(old_alias, new_alias).execute()?;
writeln!(ctx.writer, "Renamed prefix {old_alias} to {new_alias}")?;
Ok(())
},
[copy, old_alias, new_alias] if copy == "copy" => {
ctx.rudof.copy_prefix(old_alias, new_alias).execute()?;
writeln!(ctx.writer, "Copied prefix {old_alias} to {new_alias}")?;
Ok(())
},
_ => {
writeln!(
ctx.writer,
"Usage: prefixes [add ALIAS IRI | rm ALIAS | rename OLD NEW | copy OLD NEW]"
)?;
Ok(())
},
}
}
fn show_prefixes(ctx: &mut CommandContext) -> Result<()> {
let prefixes = ctx.rudof.prefixes().execute();
if prefixes.is_empty() {
writeln!(ctx.writer, "No default prefixes are defined.")?;
return Ok(());
}
write!(ctx.writer, "{prefixes}")?;
Ok(())
}
fn handle_config(args: &[String], ctx: &mut CommandContext) -> Result<()> {
match args {
[get] if get == "get" => show_config(None, ctx),
[get, key] if get == "get" => show_config(Some(key), ctx),
[set, key, value] if set == "set" => set_config(key, value, ctx),
_ => {
writeln!(ctx.writer, "Usage: config get [KEY] | config set KEY VALUE")?;
Ok(())
},
}
}
fn effective_config_root(ctx: &CommandContext) -> Result<toml::Value> {
let toml_str = ctx.rudof.config().execute().to_toml_string()?;
Ok(toml::from_str(&toml_str)?)
}
fn show_config(key: Option<&str>, ctx: &mut CommandContext) -> Result<()> {
let Some(key) = key else {
writeln!(ctx.writer, "{}", ctx.rudof.config().execute().to_toml_string()?)?;
return Ok(());
};
let root = effective_config_root(ctx)?;
match config_lookup(&root, key) {
Some(value) => {
writeln!(ctx.writer, "{}", format_config_value(value))?;
Ok(())
},
None => unknown_config_key(key, ctx),
}
}
fn set_config(key: &str, value: &str, ctx: &mut CommandContext) -> Result<()> {
let mut root = effective_config_root(ctx)?;
if config_set_path(&mut root, key, parse_config_scalar(value)).is_none() {
return unknown_config_key(key, ctx);
}
let updated_toml = toml::to_string(&root)?;
let updated_config =
RudofConfig::from_toml_str(&updated_toml).map_err(|err| anyhow!("invalid value for '{key}': {err}"))?;
let effective_toml = updated_config.to_toml_string()?;
let effective_root: toml::Value = toml::from_str(&effective_toml)?;
let Some(confirmed) = config_lookup(&effective_root, key) else {
return unknown_config_key(key, ctx);
};
let confirmed = confirmed.clone();
if key == "logging.level"
&& let Some(level) = confirmed.as_str()
{
crate::logging::set_level(level).map_err(|err| anyhow!("invalid value for '{key}': {err}"))?;
}
ctx.rudof.update_config(updated_config).execute();
writeln!(ctx.writer, "{key} = {}", format_config_value(&confirmed))?;
Ok(())
}
fn unknown_config_key(key: &str, ctx: &mut CommandContext) -> Result<()> {
writeln!(ctx.writer, "Unknown config key '{key}'.")?;
writeln!(ctx.writer, "Use 'config' with no arguments to see all available keys.")?;
Ok(())
}
fn config_lookup<'a>(root: &'a toml::Value, key: &str) -> Option<&'a toml::Value> {
key.split('.').try_fold(root, |current, segment| current.get(segment))
}
fn config_set_path(root: &mut toml::Value, key: &str, new_value: toml::Value) -> Option<()> {
let mut current = root;
let mut segments = key.split('.').peekable();
while let Some(segment) = segments.next() {
if segments.peek().is_none() {
current.as_table_mut()?.insert(segment.to_string(), new_value);
return Some(());
}
current = current.get_mut(segment)?;
}
None
}
fn config_key_paths(ctx: &CommandContext) -> Result<Vec<String>> {
let root = effective_config_root(ctx)?;
let mut paths = Vec::new();
collect_config_key_paths(&root, "", &mut paths);
paths.sort_unstable();
Ok(paths)
}
fn collect_config_key_paths(value: &toml::Value, prefix: &str, out: &mut Vec<String>) {
let toml::Value::Table(table) = value else {
return;
};
for (key, child) in table {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
out.push(path.clone());
collect_config_key_paths(child, &path, out);
}
}
fn parse_config_scalar(value: &str) -> toml::Value {
if let Ok(b) = value.parse::<bool>() {
toml::Value::Boolean(b)
} else if let Ok(i) = value.parse::<i64>() {
toml::Value::Integer(i)
} else if let Ok(f) = value.parse::<f64>() {
toml::Value::Float(f)
} else {
toml::Value::String(value.to_string())
}
}
fn format_config_value(value: &toml::Value) -> String {
match value {
toml::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn registered_endpoint_names(ctx: &CommandContext) -> Vec<String> {
let mut names: Vec<String> = ctx
.rudof
.config()
.execute()
.rdf_data()
.endpoints()
.keys()
.cloned()
.collect();
names.sort_unstable();
names
}
fn registered_endpoints_line(ctx: &CommandContext) -> String {
let names = registered_endpoint_names(ctx);
if names.is_empty() {
"No endpoints are registered in the config.".to_string()
} else {
format!("Registered endpoints: {}", names.join(", "))
}
}
const BARE_RESOURCE_FLAG: &[(&str, &str)] = &[
("shex", "-s"),
("shacl", "-s"),
("shapemap", "-m"),
("pgschema", "-s"),
("dctap", "-s"),
("service", "-s"),
("materialize", "-s"),
("generate", "-s"),
("rdf-config", "-s"),
("node", "-n"),
("sparql", "-q"),
];
fn run_external_command(command_line: &str, ctx: &mut CommandContext) -> Result<()> {
if command_line.is_empty() {
writeln!(ctx.writer, "No command given after '!'.")?;
return Ok(());
}
ctx.writer.flush()?;
system_shell(command_line)
.status()
.with_context(|| format!("failed to run external command: {command_line}"))?;
Ok(())
}
#[cfg(not(windows))]
fn system_shell(command_line: &str) -> Command {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
let mut command = Command::new(shell);
command.arg("-c").arg(command_line);
command
}
#[cfg(windows)]
fn system_shell(command_line: &str) -> Command {
let mut command = Command::new("cmd");
command.arg("/C").arg(command_line);
command
}
fn take_merge_flag(tokens: &mut Vec<String>) -> bool {
let had_merge = tokens.iter().any(|token| token == "--merge");
tokens.retain(|token| token != "--merge");
had_merge
}
fn apply_bare_resource_convenience(tokens: &mut Vec<String>) {
let [command, value] = tokens.as_slice() else {
return;
};
if value.starts_with('-') {
return;
}
if let Some((_, flag)) = BARE_RESOURCE_FLAG.iter().find(|(name, _)| name == command) {
tokens.insert(1, (*flag).to_string());
}
}
const RESOURCE_VALUE_FLAGS: &[&str] = &["-s", "-q", "-m"];
fn expand_prefixed_resource_tokens(tokens: &mut [String], ctx: &CommandContext) {
let mut i = 0;
while i < tokens.len() {
if RESOURCE_VALUE_FLAGS.contains(&tokens[i].as_str())
&& let Some(expanded) = tokens
.get(i + 1)
.and_then(|value| resolve_prefixed_resource(value, ctx))
{
tokens[i + 1] = expanded;
}
i += 1;
}
}
fn resolve_prefixed_resource(token: &str, ctx: &CommandContext) -> Option<String> {
let default_prefixes = ctx.rudof.prefixes().execute();
let endpoint_prefixes = active_endpoint_prefixmap(ctx);
crate::cli::prefix_expand::resolve_prefixed_resource(token, &default_prefixes, endpoint_prefixes.as_ref())
}
fn active_endpoint_prefixmap(ctx: &CommandContext) -> Option<PrefixMap> {
let name = ctx.active_endpoint.as_deref()?;
ctx.rudof
.config()
.execute()
.rdf_data()
.find_endpoint(name)
.map(|endpoint| endpoint.prefixmap().clone())
}
fn print_help(ctx: &mut CommandContext) -> Result<()> {
let help = ReplLine::command().render_long_help();
writeln!(ctx.writer, "{help}")?;
writeln!(ctx.writer, "Shell-only commands:")?;
writeln!(ctx.writer, " help, ? Show this help message")?;
writeln!(ctx.writer, " exit, quit Exit the shell")?;
writeln!(
ctx.writer,
" !<command> Run <command> in the system shell (e.g. !ls, !code file.shex)"
)?;
writeln!(
ctx.writer,
" endpoint [NAME] Show the active SPARQL endpoint, or activate a registered one"
)?;
writeln!(
ctx.writer,
" reset [TARGET...] Clear session state ({}), or everything with no argument",
RESET_TARGETS.join(", ")
)?;
writeln!(
ctx.writer,
" config get [KEY] Show the whole config, or one dotted key (e.g. shex_validator.max_steps)"
)?;
writeln!(
ctx.writer,
" config set KEY VALUE Change one config value for the rest of the session"
)?;
writeln!(
ctx.writer,
" prefixes [add ALIAS IRI | rm ALIAS | rename OLD NEW | copy OLD NEW]"
)?;
writeln!(
ctx.writer,
" Show, or manage, the default prefix declarations"
)?;
writeln!(
ctx.writer,
" data FILE --merge Merge FILE into the currently loaded data instead of replacing it"
)?;
Ok(())
}
fn command_names() -> Vec<String> {
let mut names: Vec<String> = ReplLine::command()
.get_subcommands()
.map(|cmd| cmd.get_name().to_string())
.collect();
names.push("help".to_string());
names.push("exit".to_string());
names.push("quit".to_string());
names.push("endpoint".to_string());
names.push("reset".to_string());
names.push("prefixes".to_string());
names
}
fn history_file() -> Option<std::path::PathBuf> {
dirs::home_dir().map(|home| home.join(".rudof_history"))
}
#[cfg(test)]
mod tests {
use super::panic_message;
use std::panic::AssertUnwindSafe;
#[test]
fn extracts_message_from_a_str_panic_payload() {
let payload = std::panic::catch_unwind(AssertUnwindSafe(|| panic!("boom"))).unwrap_err();
assert_eq!(panic_message(&payload), "boom");
}
#[test]
fn extracts_message_from_a_string_panic_payload() {
let payload = std::panic::catch_unwind(AssertUnwindSafe(|| panic!("boom {}", 42))).unwrap_err();
assert_eq!(panic_message(&payload), "boom 42");
}
#[test]
fn falls_back_to_a_generic_message_for_other_payload_types() {
let payload = std::panic::catch_unwind(AssertUnwindSafe(|| std::panic::panic_any(404))).unwrap_err();
assert_eq!(panic_message(&payload), "no further information available");
}
}