parlov-elicit 0.5.0

Elicitation engine: strategy selection and probe plan generation for parlov.
Documentation
//! Strategy registry: discovery and probe plan generation.
//!
//! `all_strategies` returns every registered elicitation strategy.
//! `generate_plan` applies risk and applicability filtering, then collects
//! all generated `ProbeSpec` values into an ordered plan for the scheduler.

use crate::context::ScanContext;
use crate::existence::cache_probing::{
    CpAccept, CpIfMatch, CpIfModifiedSince, CpIfNoneMatch, CpIfRange, CpIfUnmodifiedSince,
    CpRangeSatisfiable, CpRangeUnsatisfiable,
};
use crate::existence::error_message_granularity::{
    EmgAppVsServer404, EmgBola, EmgFkViolation, EmgQueryValidation, EmgSchemaValidationPatch,
    EmgSchemaValidationPut, EmgStateConflict,
};
use crate::existence::redirect_diff::{
    RdCaseVariation, RdDoubleSlash, RdPercentEncoding, RdPostTo303, RdProtocolUpgrade, RdPutTo303,
    RdSlashAppend, RdSlashStrip,
};
use crate::existence::status_code_diff::{
    AcceptElicitation, AuthStripElicitation, CaseNormalizeElicitation, ContentTypeElicitation,
    DependencyDeleteElicitation, EmptyBodyElicitation, IfMatchElicitation, IfMatchReadElicitation,
    IfNoneMatchElicitation, LowPrivilegeElicitation, RateLimitBurstElicitation,
    RateLimitHeadersElicitation, ScopeManipulationElicitation, StateTransitionElicitation,
    TrailingSlashElicitation, UniquenessElicitation,
};
use crate::strategy::Strategy;
use crate::types::ProbeSpec;

/// Returns all 39 registered elicitation strategies.
///
/// Grouped by risk level in declaration order: 26 Safe (9 status-code-diff +
/// 8 cache-probing + 3 error-message-granularity + 6 redirect-diff),
/// 10 `MethodDestructive` (4 status-code-diff + 4 error-message-granularity +
/// 2 redirect-diff), 3 `OperationDestructive`.
/// The scheduler may reorder by risk or method.
#[must_use]
pub fn all_strategies() -> Vec<Box<dyn Strategy>> {
    vec![
        // Safe — status-code-diff (9)
        Box::new(AcceptElicitation),
        Box::new(IfNoneMatchElicitation),
        Box::new(IfMatchReadElicitation),
        Box::new(TrailingSlashElicitation),
        Box::new(CaseNormalizeElicitation),
        Box::new(AuthStripElicitation),
        Box::new(LowPrivilegeElicitation),
        Box::new(ScopeManipulationElicitation),
        Box::new(RateLimitHeadersElicitation),
        // Safe — cache-probing (8)
        Box::new(CpIfNoneMatch),
        Box::new(CpIfModifiedSince),
        Box::new(CpIfMatch),
        Box::new(CpIfUnmodifiedSince),
        Box::new(CpRangeSatisfiable),
        Box::new(CpRangeUnsatisfiable),
        Box::new(CpIfRange),
        Box::new(CpAccept),
        // Safe — error-message-granularity (3)
        Box::new(EmgBola),
        Box::new(EmgQueryValidation),
        Box::new(EmgAppVsServer404),
        // Safe — redirect-diff (6)
        Box::new(RdSlashAppend),
        Box::new(RdSlashStrip),
        Box::new(RdCaseVariation),
        Box::new(RdDoubleSlash),
        Box::new(RdPercentEncoding),
        Box::new(RdProtocolUpgrade),
        // MethodDestructive — status-code-diff (4)
        Box::new(ContentTypeElicitation),
        Box::new(IfMatchElicitation),
        Box::new(EmptyBodyElicitation),
        Box::new(StateTransitionElicitation),
        // MethodDestructive — error-message-granularity (4)
        Box::new(EmgSchemaValidationPatch),
        Box::new(EmgSchemaValidationPut),
        Box::new(EmgStateConflict),
        Box::new(EmgFkViolation),
        // MethodDestructive — redirect-diff (2)
        Box::new(RdPostTo303),
        Box::new(RdPutTo303),
        // OperationDestructive (3)
        Box::new(UniquenessElicitation),
        Box::new(DependencyDeleteElicitation),
        Box::new(RateLimitBurstElicitation),
    ]
}

/// Filters `strategies` to those within `ctx.max_risk` and applicable to `ctx`.
///
/// Owns the shared filter chain used by both `generate_plan` and
/// `generate_chained_plan`. Callers `flat_map` over the result with their
/// terminal closure.
pub(crate) fn applicable_strategies<'a>(
    strategies: &'a [Box<dyn Strategy>],
    ctx: &'a ScanContext,
) -> impl Iterator<Item = &'a dyn Strategy> {
    strategies
        .iter()
        .map(Box::as_ref)
        .filter(move |s| s.risk() <= ctx.max_risk)
        .filter(move |s| s.is_applicable(ctx))
}

/// Generates a filtered probe plan for `ctx`.
///
/// Iterates `all_strategies()`, skips any strategy where
/// `strategy.risk() > ctx.max_risk` or `!strategy.is_applicable(ctx)`, and
/// collects the union of all `generate()` results in declaration order.
#[must_use]
pub fn generate_plan(ctx: &ScanContext) -> Vec<ProbeSpec> {
    let strategies = all_strategies();
    applicable_strategies(&strategies, ctx)
        .flat_map(|s| s.generate(ctx))
        .collect()
}

#[cfg(test)]
#[path = "registry_tests.rs"]
mod tests;