//! Defines the query struct and related types. //! Query parsing is handled in the `parser` module use std::collections::HashSet; use crate::expr::Expr; use crate::field::Field; use crate::query::TraversalMode::Bfs; #[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)] /// Represents a query to be executed on . /// pub struct Query { /// File fields to be selected pub fields: Vec, /// Root directories to search pub roots: Vec, /// "where" filter expression pub expr: Option, /// Fields to group by pub grouping_fields: Vec, /// Fields to order by pub ordering_fields: Vec, /// Ordering direction (true for asc, false for desc) pub ordering_asc: Vec, /// Max amount of results to return pub limit: u32, pub offset: u32, /// Output format pub output_format: OutputFormat, pub raw_query: String, } impl Query { pub fn get_all_fields(&self) -> HashSet { let mut result = HashSet::new(); for column_expr in &self.fields { result.extend(column_expr.get_required_fields()); } // Ordering expressions may reference columns that are not selected // (e.g. `select ext, count(*) ... order by max(modified)`); the // aggregation scan must collect their per-file values too. for ordering_expr in &self.ordering_fields { result.extend(ordering_expr.get_required_fields()); } result } pub fn is_ordered(&self) -> bool { !self.ordering_fields.is_empty() } pub fn has_aggregate_column(&self) -> bool { self.fields.iter().any(|f| f.has_aggregate_function()) } /// True when results are produced per group rather than per file: either /// an aggregate column is selected, or GROUP BY is present. Like in SQL, /// `select ext from dir group by ext` collapses rows into distinct groups /// even though no aggregate function appears in the SELECT list. pub fn is_aggregated(&self) -> bool { self.has_aggregate_column() || !self.grouping_fields.is_empty() } } #[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)] /// Represents a root directory to start the search from, with traversal options. /// When `subquery` is present, the root represents the result set of an inner /// query rather than a filesystem path; `path` is unused in that case. pub struct Root { pub path: String, pub options: RootOptions, pub subquery: Option>, } macro_rules! root_options { ( $(#[$struct_attrs:meta])* $vis:vis struct $struct_name:ident { $( $( @text = [$($text:literal),*], description = $description:literal )+ $(#[$field_attrs:meta])* $field_vis:vis $field:ident: $field_type:ty ),* $(,)? } ) => { $(#[$struct_attrs])* $vis struct $struct_name { $( $(#[$field_attrs])* $field_vis $field: $field_type, )* } impl $struct_name { pub fn get_names_and_descriptions() -> Vec<(Vec<&'static str>, &'static str)> { vec![ $( $(#[$field_attrs])* $( (vec![$($text,)*], $description), )+ )* ] } } }; } root_options! { #[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)] pub struct RootOptions { @text = ["mindepth"], description = "Minimum depth to search" pub min_depth: u32, @text = ["maxdepth", "depth"], description = "Maximum depth to search" pub max_depth: u32, @text = ["archives", "arc"], description = "Whether to search archives" pub archives: bool, @text = ["symlinks", "sym"], description = "Whether to follow symlinks" pub symlinks: bool, @text = ["gitignore", "git"], description = "Search respects .gitignore files found" @text = ["nogitignore", "nogit"], description = "Disable .gitignore parsing during the search" pub gitignore: Option, @text = ["hgignore", "hg"], description = "Search respects .hgignore files found" @text = ["nohgignore", "nohg"], description = "Disable .hgignore parsing during the search" pub hgignore: Option, @text = ["dockerignore", "docker"], description = "Search respects .dockerignore files found" @text = ["nodockerignore", "nodocker"], description = "Disable .dockerignore parsing during the search" pub dockerignore: Option, @text = ["dfs"], description = "Depth-first search mode" @text = ["bfs"], description = "Breadth-first search mode (default)" pub traversal: TraversalMode, @text = ["regexp", "rx"], description = "Treat the path as a regular expression" pub regexp: bool, @text = ["as"], description = "Alias for the root path" pub alias: Option, } } impl RootOptions { pub fn new() -> RootOptions { RootOptions { min_depth: 0, max_depth: 0, archives: false, symlinks: false, gitignore: None, hgignore: None, dockerignore: None, traversal: Bfs, regexp: false, alias: None, } } #[cfg(test)] #[allow(clippy::too_many_arguments)] pub fn from( min_depth: u32, max_depth: u32, archives: bool, symlinks: bool, gitignore: Option, hgignore: Option, dockerignore: Option, traversal: TraversalMode, regexp: bool, alias: Option, ) -> RootOptions { RootOptions { min_depth, max_depth, archives, symlinks, gitignore, hgignore, dockerignore, traversal, regexp, alias, } } } impl Root { pub fn new(path: String, options: RootOptions) -> Root { Root { path, options, subquery: None } } pub fn from_subquery(subquery: Query, options: RootOptions) -> Root { Root { path: String::new(), options, subquery: Some(Box::new(subquery)), } } pub fn default(options: Option) -> Root { Root { path: String::from("."), options: options.unwrap_or_else(RootOptions::new), subquery: None, } } pub fn clone_with_path(new_path: String, source: Root) -> Root { Root { path: new_path, ..source } } #[cfg(test)] pub fn is_subquery(&self) -> bool { self.subquery.is_some() } } #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Hash, Serialize)] pub enum TraversalMode { Bfs, Dfs, } macro_rules! output_format { ( $(#[$enum_attrs:meta])* $vis:vis enum $enum_name:ident { $( @text = $text:literal @description = $description:literal $(#[$variant_attrs:meta])* $(@supports_colorization = $colorized:literal)? $variant:ident$(,)? )* } ) => { $(#[$enum_attrs])* $vis enum $enum_name { $( $(#[$variant_attrs])* $variant, )* } impl $enum_name { pub fn from(s: &str) -> Option { let s = s.to_lowercase(); match s.as_str() { $( $text => Some($enum_name::$variant), )* _ => None, } } pub fn get_names_and_descriptions() -> Vec<(&'static str, &'static str)> { vec![ $( ($text, $description), )* ] } pub fn supports_colorization(&self) -> bool { match self { $( $(#[$variant_attrs])* $enum_name::$variant => { false $(|| stringify!($colorized) == "true")? } )* } } } }; } output_format! { #[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Serialize)] pub enum OutputFormat { @text = "tabs" @description = "Tab-separated values (default)" @supports_colorization = true Tabs, @text = "lines" @description = "One item per line" @supports_colorization = true Lines, @text = "list" @description = "Entire output onto a single line for xargs" List, @text = "csv" @description = "Comma-separated values" Csv, @text = "json" @description = "JSON format" Json, @text = "html" @description = "HTML format" Html, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_supports_colorization_tabs() { assert!( OutputFormat::Tabs.supports_colorization(), "Tabs should support colorization" ); } #[test] fn test_supports_colorization_lines() { assert!( OutputFormat::Lines.supports_colorization(), "Lines should support colorization" ); } #[test] fn test_no_colorization_csv() { assert!( !OutputFormat::Csv.supports_colorization(), "Csv should not support colorization" ); } }