// Composable path-query engine for build systems and source explorers.
///|
/// A report returned by a path query.
pub(all) struct QueryReport {
examined : Int
included : Int
excluded : Int
returned : Int
truncated : Bool
results : Array[String]
} derive(Debug, Eq)
///|
/// Returns whether the query produced at least one result.
pub fn QueryReport::has_results(self : QueryReport) -> Bool {
self.returned > 0
}
///|
/// Returns whether the result set contains every accepted path.
pub fn QueryReport::is_complete(self : QueryReport) -> Bool {
!self.truncated
}
///|
/// Returns the number of results in the current pageable report.
pub fn QueryReport::returned_count(self : QueryReport) -> Int {
self.returned
}
///|
/// Returns a bounded page of results without changing the original report.
/// Invalid offsets and page sizes produce an empty page.
pub fn QueryReport::page(
self : QueryReport,
offset : Int,
page_size : Int,
) -> Array[String] {
let page : Array[String] = []
if offset < 0 || page_size <= 0 || offset >= self.results.length() {
return page
}
let end = if offset + page_size < self.results.length() {
offset + page_size
} else {
self.results.length()
}
for i in offset.. String {
"examined=" +
self.examined.to_string() +
",included=" +
self.included.to_string() +
",excluded=" +
self.excluded.to_string() +
",returned=" +
self.returned.to_string() +
",truncated=" +
self.truncated.to_string()
}
///|
/// A reusable query with include rules, exclude rules, and result policies.
pub(all) struct GlobQuery {
includes : Array[CompiledPattern]
excludes : Array[CompiledPattern]
options : GlobOptions
min_depth : Int
limit : Int?
} derive(Debug, Eq)
///|
pub fn GlobQuery::new() -> GlobQuery {
{
includes: [],
excludes: [],
options: GlobOptions::default(),
min_depth: 0,
limit: None,
}
}
///|
/// Adds an include pattern. Multiple includes use OR semantics.
pub fn GlobQuery::include_pattern(
self : GlobQuery,
pattern : String,
) -> Result[GlobQuery, GlobError] {
match compile_pattern(pattern) {
Err(err) => Err(err)
Ok(compiled) => {
let includes = self.includes.copy()
includes.push(compiled)
Ok({ ..self, includes, })
}
}
}
///|
/// Adds an exclude pattern. Exclusions always take precedence.
pub fn GlobQuery::exclude_pattern(
self : GlobQuery,
pattern : String,
) -> Result[GlobQuery, GlobError] {
match compile_pattern(pattern) {
Err(err) => Err(err)
Ok(compiled) => {
let excludes = self.excludes.copy()
excludes.push(compiled)
Ok({ ..self, excludes, })
}
}
}
///|
/// Replaces traversal and result options.
pub fn GlobQuery::with_options(
self : GlobQuery,
options : GlobOptions,
) -> GlobQuery {
{ ..self, options, }
}
///|
/// Requires paths to have at least the given number of components.
pub fn GlobQuery::with_min_depth(
self : GlobQuery,
depth : Int,
) -> Result[GlobQuery, GlobError] {
if depth < 0 {
Err(GlobError::InvalidMaxDepth)
} else {
Ok({ ..self, min_depth: depth })
}
}
///|
/// Limits the number of returned paths.
pub fn GlobQuery::with_limit(
self : GlobQuery,
limit : Int,
) -> Result[GlobQuery, GlobError] {
if limit < 0 {
Err(GlobError::InvalidMaxDepth)
} else {
Ok({ ..self, limit: Some(limit) })
}
}
///|
/// Enables deterministic result ordering.
pub fn GlobQuery::sorted(self : GlobQuery) -> GlobQuery {
{ ..self, options: self.options.sorted() }
}
///|
/// Excludes hidden components from the result.
pub fn GlobQuery::without_hidden(self : GlobQuery) -> GlobQuery {
{ ..self, options: self.options.without_hidden() }
}
///|
/// Restricts results to files when querying a filesystem.
pub fn GlobQuery::files_only(self : GlobQuery) -> GlobQuery {
{ ..self, options: self.options.files_only() }
}
///|
/// Restricts results to directories when querying a filesystem.
pub fn GlobQuery::directories_only(self : GlobQuery) -> GlobQuery {
{ ..self, options: self.options.directories_only() }
}
///|
/// Returns the number of include rules.
pub fn GlobQuery::include_count(self : GlobQuery) -> Int {
self.includes.length()
}
///|
/// Returns the number of exclude rules.
pub fn GlobQuery::exclude_count(self : GlobQuery) -> Int {
self.excludes.length()
}
///|
fn query_matches_include(query : GlobQuery, path : String) -> Bool {
if query.includes.is_empty() {
return true
}
match_compiled_any(query.includes, path)
}
///|
fn query_matches_exclude(query : GlobQuery, path : String) -> Bool {
match_compiled_any(query.excludes, path)
}
///|
fn query_within_depth(query : GlobQuery, path : String) -> Bool {
let depth = path_depth(path)
if depth < query.min_depth {
return false
}
match query.options.max_depth {
None => true
Some(max_depth) => depth <= max_depth
}
}
///|
fn query_accepts(query : GlobQuery, path : String) -> Bool {
if !query.options.include_hidden && is_hidden_path(path) {
return false
}
query_within_depth(query, path) && query_matches_include(query, path)
}
///|
/// Applies query type filters while retaining the metadata held by an index.
fn query_accepts_record(query : GlobQuery, entry : PathRecord) -> Bool {
if entry.is_file() && !query.options.include_files {
return false
}
if entry.is_directory() && !query.options.include_directories {
return false
}
query_accepts(query, entry.path)
}
///|
/// Tests a normalized or platform-specific path against this query.
pub fn GlobQuery::matches(self : GlobQuery, path : String) -> Bool {
let normalized = normalize_path(path)
query_accepts(self, normalized) && !query_matches_exclude(self, normalized)
}
///|
/// Executes this query against a caller-provided path list.
pub fn GlobQuery::execute_paths(
self : GlobQuery,
paths : Array[String],
) -> QueryReport {
let mut examined = 0
let mut included = 0
let mut excluded = 0
let mut truncated = false
let results : Array[String] = []
let seen : Array[String] = []
for path in paths {
examined = examined + 1
let normalized = normalize_path(path)
if query_accepts(self, normalized) {
if query_matches_exclude(self, normalized) {
excluded = excluded + 1
} else {
included = included + 1
if !seen.contains(normalized) {
seen.push(normalized)
match self.limit {
Some(limit) if results.length() >= limit => truncated = true
_ => results.push(normalized)
}
}
}
}
}
if self.options.sort_results {
results.sort()
}
{
examined,
included,
excluded,
returned: results.length(),
truncated,
results,
}
}
///|
/// Executes a query against an in-memory index without discarding entry kinds.
/// This is useful for build tools that refresh a path index once and issue many
/// queries without repeatedly touching the filesystem.
pub fn GlobQuery::execute_index(
self : GlobQuery,
index : PathIndex,
) -> QueryReport {
let mut examined = 0
let mut included = 0
let mut excluded = 0
let mut truncated = false
let results : Array[String] = []
let seen : Array[String] = []
for entry in index.records() {
examined = examined + 1
if query_accepts_record(self, entry) {
if query_matches_exclude(self, entry.path) {
excluded = excluded + 1
} else {
included = included + 1
if !seen.contains(entry.path) {
seen.push(entry.path)
match self.limit {
Some(limit) if results.length() >= limit => truncated = true
_ => results.push(entry.path)
}
}
}
}
}
if self.options.sort_results {
results.sort()
}
{
examined,
included,
excluded,
returned: results.length(),
truncated,
results,
}
}
///|
/// Executes the query against all traversable entries under a directory.
pub fn GlobQuery::execute_filesystem(
self : GlobQuery,
dir : String,
) -> Result[QueryReport, GlobError] {
let traversal_pattern = if self.includes.is_empty() { "**/*" } else { "**/*" }
match glob_with_options(dir, traversal_pattern, self.options) {
Err(err) => Err(err)
Ok(paths) => Ok(self.execute_paths(paths))
}
}
///|
/// Returns a concise configuration summary for logs and diagnostics.
pub fn GlobQuery::summary(self : GlobQuery) -> String {
let max_depth = match self.options.max_depth {
None => "none"
Some(depth) => depth.to_string()
}
let limit = match self.limit {
None => "none"
Some(value) => value.to_string()
}
"includes=" +
self.include_count().to_string() +
",excludes=" +
self.exclude_count().to_string() +
",min_depth=" +
self.min_depth.to_string() +
",max_depth=" +
max_depth +
",limit=" +
limit +
",hidden=" +
self.options.include_hidden.to_string() +
",sorted=" +
self.options.sort_results.to_string()
}
///|
/// Creates a query from include and exclude source patterns.
pub fn build_query(
includes : Array[String],
excludes : Array[String],
) -> Result[GlobQuery, GlobError] {
let mut query = GlobQuery::new()
for pattern in includes {
match query.include_pattern(pattern) {
Ok(next) => query = next
Err(err) => return Err(err)
}
}
for pattern in excludes {
match query.exclude_pattern(pattern) {
Ok(next) => query = next
Err(err) => return Err(err)
}
}
Ok(query)
}
///|
/// Executes a one-shot include/exclude query over a path list.
pub fn query_paths(
includes : Array[String],
excludes : Array[String],
paths : Array[String],
) -> Result[QueryReport, GlobError] {
match build_query(includes, excludes) {
Err(err) => Err(err)
Ok(query) => Ok(query.execute_paths(paths))
}
}