use super::universe::{self, CeilingKind, FilterScope, QueryCeiling};
use super::vocabulary::{KeyOrigin, Scope};
use super::AgentSurface;
use crate::errors::AppError;
use crate::i18n::validation as msg;
#[derive(Debug, Default)]
pub struct Findings {
pub resolved_keys: Vec<String>,
pub unresolved_keys: Vec<String>,
pub key_suggestions: Vec<String>,
pub vocabulary_partial: bool,
}
impl Findings {
pub fn is_partial(&self) -> bool {
!self.unresolved_keys.is_empty()
}
}
const ANONYMOUS_ARRAY: &str = "results";
const FILTER_FLAG: &str = "--filter";
const SORT_FLAG: &str = "--sort";
const DEDUPE_FLAG: &str = "--dedupe-by";
const SELECT_FLAG: &str = "--select";
const COUNT_ONLY_FLAG: &str = "--count-only";
const MAX_OUTPUT_BYTES_FLAG: &str = "--max-output-bytes";
const MAX_ITEMS_FLAG: &str = "--max-items";
fn refuse(message: String, discarded_flags: Vec<String>) -> AppError {
AppError::Usage {
message,
discarded_flags,
}
}
pub fn evaluate(
surface: &AgentSurface,
scope: &Scope,
array_key: Option<&str>,
has_array: bool,
ceiling: Option<&universe::QueryCeiling>,
) -> Result<Findings, AppError> {
let mut findings = Findings::default();
if surface.mutates {
return Ok(findings);
}
refuse_whole_set_knobs_on_a_stream(surface)?;
refuse_inert_knobs(surface, has_array)?;
refuse_a_predicate_over_a_page(surface, ceiling)?;
refuse_a_count_over_a_page(surface, ceiling)?;
if surface.allow_unknown_keys {
return Ok(findings);
}
if has_array && scope.is_empty() {
return Ok(findings);
}
let array_name = array_key.unwrap_or(ANONYMOUS_ARRAY);
for expr in &surface.filters {
refuse_unusable_key(scope, FILTER_FLAG, &expr.key(), array_name)?;
}
if let Some(key) = &surface.sort {
refuse_unusable_key(scope, SORT_FLAG, key, array_name)?;
}
if let Some(key) = &surface.dedupe_by {
refuse_unusable_key(scope, DEDUPE_FLAG, key, array_name)?;
}
let declared_array = array_key.is_some_and(super::is_declared_result_array);
resolve_projection(surface, scope, has_array && declared_array, &mut findings)?;
Ok(findings)
}
pub fn evaluate_stream(surface: &AgentSurface, scope: &Scope) -> Result<Findings, AppError> {
let mut findings = Findings::default();
if surface.mutates {
return Ok(findings);
}
refuse_whole_set_knobs(surface)?;
refuse_a_predicate_on_a_stream(surface)?;
if surface.allow_unknown_keys || scope.is_empty() {
return Ok(findings);
}
resolve_projection(surface, scope, true, &mut findings)?;
Ok(findings)
}
fn a_truncated_page<'a>(
surface: &AgentSurface,
ceiling: Option<&'a QueryCeiling>,
) -> Option<&'a QueryCeiling> {
let ceiling = ceiling?;
if ceiling.kind != CeilingKind::Pagination || !ceiling.truncated_the_universe() {
return None;
}
if surface.filter_scope == Some(FilterScope::Page) {
return None;
}
Some(ceiling)
}
fn refuse_a_predicate_over_a_page(
surface: &AgentSurface,
ceiling: Option<&QueryCeiling>,
) -> Result<(), AppError> {
if surface.filters.is_empty() {
return Ok(());
}
let Some(ceiling) = a_truncated_page(surface, ceiling) else {
return Ok(());
};
let total = ceiling.universe_total.unwrap_or(ceiling.applied);
Err(refuse(
msg::filter_scope_is_a_page(ceiling.applied, total, ceiling.source.as_str()),
vec![FILTER_FLAG.to_string()],
))
}
fn refuse_a_count_over_a_page(
surface: &AgentSurface,
ceiling: Option<&QueryCeiling>,
) -> Result<(), AppError> {
if !surface.count_only {
return Ok(());
}
let Some(ceiling) = a_truncated_page(surface, ceiling) else {
return Ok(());
};
let total = ceiling.universe_total.unwrap_or(ceiling.applied);
Err(refuse(
msg::count_only_over_a_page(ceiling.applied, total),
vec![COUNT_ONLY_FLAG.to_string()],
))
}
fn refuse_whole_set_knobs_on_a_stream(surface: &AgentSurface) -> Result<(), AppError> {
if !surface.streamed {
return Ok(());
}
refuse_whole_set_knobs(surface)?;
refuse_a_predicate_on_a_stream(surface)
}
fn refuse_whole_set_knobs(surface: &AgentSurface) -> Result<(), AppError> {
let mut discarded = Vec::new();
if surface.count_only {
discarded.push(COUNT_ONLY_FLAG.to_string());
}
if surface.sort.is_some() {
discarded.push(SORT_FLAG.to_string());
}
if surface.dedupe_by.is_some() {
discarded.push(DEDUPE_FLAG.to_string());
}
if surface.max_output_bytes > 0 {
discarded.push(MAX_OUTPUT_BYTES_FLAG.to_string());
}
if surface.max_items > 0 {
discarded.push(MAX_ITEMS_FLAG.to_string());
}
if discarded.is_empty() {
return Ok(());
}
Err(refuse(msg::knob_needs_a_whole_set(&discarded), discarded))
}
fn refuse_a_predicate_on_a_stream(surface: &AgentSurface) -> Result<(), AppError> {
if surface.filters.is_empty() {
return Ok(());
}
let discarded = vec![FILTER_FLAG.to_string()];
Err(refuse(msg::filter_would_desync_a_tally(), discarded))
}
fn refuse_inert_knobs(surface: &AgentSurface, has_array: bool) -> Result<(), AppError> {
if has_array {
return Ok(());
}
let mut discarded = Vec::new();
if !surface.filters.is_empty() {
discarded.push(FILTER_FLAG.to_string());
}
if surface.sort.is_some() {
discarded.push(SORT_FLAG.to_string());
}
if surface.dedupe_by.is_some() {
discarded.push(DEDUPE_FLAG.to_string());
}
if discarded.is_empty() {
return Ok(());
}
Err(refuse(msg::knob_without_target(&discarded), discarded))
}
fn refuse_unusable_key(
scope: &Scope,
flag: &str,
key: &str,
array_name: &str,
) -> Result<(), AppError> {
match scope.classify(key) {
KeyOrigin::Element => Ok(()),
KeyOrigin::EnvelopeOnly => Err(refuse(
msg::key_is_envelope_only(flag, key, array_name),
vec![flag.to_string()],
)),
KeyOrigin::Absent => Err(refuse(
msg::key_absent(flag, key, &scope.suggestions(key)),
vec![flag.to_string()],
)),
}
}
fn resolve_projection(
surface: &AgentSurface,
scope: &Scope,
has_array: bool,
findings: &mut Findings,
) -> Result<(), AppError> {
if surface.select.is_empty() {
return Ok(());
}
let usable = if has_array {
KeyOrigin::Element
} else {
KeyOrigin::EnvelopeOnly
};
for key in &surface.select {
if scope.classify(key) == usable {
findings.resolved_keys.push(key.clone());
} else {
findings.unresolved_keys.push(key.clone());
}
}
if !findings.resolved_keys.is_empty() {
if !findings.unresolved_keys.is_empty() {
findings.vocabulary_partial = scope.vocabulary_is_partial();
let mut seen = std::collections::BTreeSet::new();
for key in &findings.unresolved_keys {
for candidate in scope.suggestions(key) {
seen.insert(candidate);
}
}
findings.key_suggestions = seen.into_iter().collect();
}
return Ok(());
}
let suggestions = surface
.select
.first()
.map(|key| scope.suggestions(key))
.unwrap_or_default();
Err(refuse(
msg::select_fully_unresolved(&surface.select, &suggestions),
vec![SELECT_FLAG.to_string()],
))
}