use crate::presentation::ResultListPresentation;
use crate::query::QueryPlan;
use crate::query::QueryResultRow;
use crate::query::QueryRuntime;
use arbitrary::Arbitrary;
use eyre::ensure;
use facet::Facet;
use figue::{self as args};
use std::io::IsTerminal;
use tracing::debug;
use tracing::instrument;
#[derive(Facet, PartialEq, Debug, Arbitrary, Default, Clone)]
#[facet(rename_all = "kebab-case")]
pub struct QueryArgs {
#[facet(flatten)]
pub plan: QueryPlan,
#[facet(args::named, default)]
pub density: QueryResultsOutputDensity,
#[facet(args::named, default)]
pub no_daemon: bool,
#[facet(args::named, default)]
pub daemon: bool,
}
#[derive(Default, Facet, Arbitrary, Clone, Copy, Debug, Eq, PartialEq, strum::Display)]
#[repr(u8)]
#[strum(serialize_all = "kebab-case")]
#[facet(rename_all = "kebab-case")]
pub enum QueryResultsOutputDensity {
#[default]
Auto,
Lines,
Columns,
}
impl QueryArgs {
pub fn new(pattern: impl Into<String>) -> Self {
Self {
plan: QueryPlan::new(pattern),
..Default::default()
}
}
#[instrument(level = "info", skip_all, fields(query = ?self.plan.query, query_scope = ?self.plan.r#in, profile = ?self.plan.profile, limit = ?self.plan.limit, include_deleted = self.plan.include_deleted, only_deleted = self.plan.only_deleted, show_filtered = self.plan.show_filtered, only_filtered = self.plan.only_filtered, density = ?self.density))]
pub fn invoke_and_print(self) -> eyre::Result<()> {
let results = self.collect_rows()?;
let stdout_is_terminal = std::io::stdout().is_terminal();
let colorize = stdout_is_terminal
&& (self.plan.include_deleted
|| self.plan.only_deleted
|| self.plan.show_filtered
|| self.plan.only_filtered);
let result_limit = self
.plan
.limit
.map(std::convert::Into::into)
.unwrap_or(results.len())
.min(results.len());
let display_results = &results[..result_limit];
let presentation = ResultListPresentation::for_terminal();
let mut stdout = std::io::stdout().lock();
let use_columns = match self.density {
QueryResultsOutputDensity::Auto => stdout_is_terminal,
QueryResultsOutputDensity::Lines => false,
QueryResultsOutputDensity::Columns => true,
};
presentation.write_result_list(
display_results,
&mut stdout,
use_columns,
|row| row.path.as_str().chars().count(),
|row, writer| row.render_path(writer, colorize),
)?;
Ok(())
}
pub fn check_query(&self) -> eyre::Result<()> {
if self.plan.query.is_empty() {
eyre::bail!("query must not be empty");
}
for (index, group) in self.plan.query.groups().iter().enumerate() {
if group.is_empty() {
eyre::bail!("query argument {index} is empty; pass a non-empty query string");
}
for (rule_index, rule) in group.rules.iter().enumerate() {
if rule.is_empty() {
tracing::warn!(
query_index = index,
rule_index = rule_index,
query = ?group,
"Query rule contains only whitespace"
);
}
}
}
Ok(())
}
#[allow(
clippy::too_many_lines,
reason = "This method centralizes the query source selection behavior"
)]
pub fn collect_rows(&self) -> eyre::Result<Vec<QueryResultRow>> {
debug!("Running query with args: {:?}", self);
self.check_query()?;
ensure!(
!(self.daemon && self.no_daemon),
"`--daemon` and `--no-daemon` cannot be used together"
);
self.plan.ensure_selected_profile_allowed()?;
let rtn = self.query_runtime().collect_rows(self.plan.clone())?;
if let Some(limit) = **self.plan.limit {
ensure!(
rtn.len() <= limit.into(),
"Collected more results ({}) than the specified limit ({})",
rtn.len(),
limit
);
}
Ok(rtn)
}
fn query_runtime(&self) -> QueryRuntime {
if self.daemon {
QueryRuntime::daemon_rpc()
} else {
QueryRuntime::published_index_only()
}
}
}
#[cfg(test)]
mod tests {
use super::QueryArgs;
use crate::query::QueryRuntime;
#[test]
fn default_and_no_daemon_query_args_use_published_index_runtime() {
let default_args = QueryArgs::new("Cargo.toml");
let no_daemon_args = QueryArgs {
no_daemon: true,
..QueryArgs::new("Cargo.toml")
};
assert_eq!(
default_args.query_runtime(),
QueryRuntime::PublishedIndexOnly
);
assert_eq!(
no_daemon_args.query_runtime(),
QueryRuntime::PublishedIndexOnly
);
}
#[test]
fn daemon_query_args_use_daemon_runtime() {
let args = QueryArgs {
daemon: true,
..QueryArgs::new("Cargo.toml")
};
assert_eq!(args.query_runtime(), QueryRuntime::DaemonRpc);
}
#[test]
fn conflicting_daemon_flags_fail_before_runtime_access() {
let args = QueryArgs {
daemon: true,
no_daemon: true,
..QueryArgs::new("Cargo.toml")
};
let error = args
.collect_rows()
.expect_err("conflicting daemon flags should fail early");
assert!(
error
.to_string()
.contains("`--daemon` and `--no-daemon` cannot be used together")
);
}
}