use std::sync::Arc;
use bytes::Bytes;
use http::HeaderMap;
use parlov_core::ResponseClass;
use crate::context::ScanContext;
use crate::harvest::EtagStrength;
use crate::types::{ChainProvenance, ProbeSpec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProducerOutputKind {
Etag,
LastModified,
Location,
ContentRangeSize,
AcceptRanges,
ResourceId,
ProblemDetails,
ContentType,
AuthChallenge,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProducerOutput {
Etag(String, EtagStrength),
LastModified(String),
Location(String),
ContentRangeSize(u64),
AcceptRanges(String),
ResourceId(String),
ProblemDetails {
required_fields: Vec<String>,
error_type: Option<String>,
},
ContentType(String),
AuthChallenge {
scheme: String,
realm: Option<String>,
scope: Option<String>,
},
}
impl ProducerOutput {
#[must_use]
pub fn kind(&self) -> ProducerOutputKind {
match self {
Self::Etag(..) => ProducerOutputKind::Etag,
Self::LastModified(..) => ProducerOutputKind::LastModified,
Self::Location(..) => ProducerOutputKind::Location,
Self::ContentRangeSize(..) => ProducerOutputKind::ContentRangeSize,
Self::AcceptRanges(..) => ProducerOutputKind::AcceptRanges,
Self::ResourceId(..) => ProducerOutputKind::ResourceId,
Self::ProblemDetails { .. } => ProducerOutputKind::ProblemDetails,
Self::ContentType(..) => ProducerOutputKind::ContentType,
Self::AuthChallenge { .. } => ProducerOutputKind::AuthChallenge,
}
}
#[must_use]
pub fn kind_string(&self) -> String {
match self.kind() {
ProducerOutputKind::Etag => "Etag",
ProducerOutputKind::LastModified => "LastModified",
ProducerOutputKind::Location => "Location",
ProducerOutputKind::ContentRangeSize => "ContentRangeSize",
ProducerOutputKind::AcceptRanges => "AcceptRanges",
ProducerOutputKind::ResourceId => "ResourceId",
ProducerOutputKind::ProblemDetails => "ProblemDetails",
ProducerOutputKind::ContentType => "ContentType",
ProducerOutputKind::AuthChallenge => "AuthChallenge",
}
.to_owned()
}
#[must_use]
pub fn value_string(&self) -> String {
match self {
Self::Etag(s, _)
| Self::LastModified(s)
| Self::Location(s)
| Self::AcceptRanges(s)
| Self::ResourceId(s)
| Self::ContentType(s) => s.clone(),
Self::ContentRangeSize(n) => n.to_string(),
Self::ProblemDetails {
required_fields,
error_type,
} => format!(
"required_fields={required_fields:?} type={}",
error_type.as_deref().unwrap_or("-")
),
Self::AuthChallenge {
scheme,
realm,
scope,
} => format!(
"scheme={scheme} realm={} scope={}",
realm.as_deref().unwrap_or("-"),
scope.as_deref().unwrap_or("-"),
),
}
}
}
pub trait Producer: Send + Sync {
fn admits(&self, class: ResponseClass) -> bool;
fn extract(&self, class: ResponseClass, headers: &HeaderMap) -> Option<ProducerOutput>;
fn extract_with_body(
&self,
class: ResponseClass,
headers: &HeaderMap,
_body: &Bytes,
) -> Option<ProducerOutput> {
self.extract(class, headers)
}
}
pub trait Consumer: Send + Sync {
fn needs(&self) -> ProducerOutputKind;
fn generate(&self, ctx: &ScanContext, output: &ProducerOutput) -> Vec<ProbeSpec>;
}
pub struct ChainRegistry {
edges: Vec<(Arc<dyn Producer>, Arc<dyn Consumer>)>,
}
impl ChainRegistry {
#[must_use]
pub fn new() -> Self {
Self { edges: Vec::new() }
}
pub fn register(&mut self, producer: Arc<dyn Producer>, consumer: Arc<dyn Consumer>) {
self.edges.push((producer, consumer));
}
#[must_use]
pub fn len(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
pub fn extend(&mut self, other: Self) {
self.edges.extend(other.edges);
}
}
impl Default for ChainRegistry {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn generate_dag_chained_plan(
ctx: &ScanContext,
exchanges: &[(ResponseClass, HeaderMap, Bytes)],
registry: &ChainRegistry,
) -> Vec<ProbeSpec> {
let mut specs = Vec::new();
for (producer, consumer) in ®istry.edges {
let output = exchanges.iter().find_map(|(class, headers, body)| {
if producer.admits(*class) {
let out = producer.extract_with_body(*class, headers, body)?;
if out.kind() == consumer.needs() {
Some(out)
} else {
None
}
} else {
None
}
});
if let Some(output) = output {
let prov = ChainProvenance {
producer_kind: output.kind_string(),
producer_value: output.value_string(),
};
for spec in consumer.generate(ctx, &output) {
specs.push(spec.with_chain_provenance(prov.clone()));
}
}
}
specs
}
#[cfg(test)]
#[path = "chain_tests.rs"]
mod tests;