#![forbid(unsafe_code)]
use sim_kernel::{ContentId, Datum, NumberLiteral};
use sim_lib_net_core::normalize_retrieval_uri;
use sim_lib_web_core::{DecodeLimits, EvidenceSelector, WebRecordError, WebRepresentation};
use std::{error::Error, fmt};
pub static RECIPES: sim_cookbook::EmbeddedDir =
include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SearchError {
EmptyQuery,
BoundExceeded(&'static str),
InvalidRecord(&'static str),
Citation(WebRecordError),
Wire(String),
}
impl fmt::Display for SearchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl Error for SearchError {}
impl From<WebRecordError> for SearchError {
fn from(value: WebRecordError) -> Self {
Self::Citation(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchQuery {
pub text: String,
pub sites: Vec<SearchSite>,
pub language: Option<String>,
pub limit: u32,
}
impl SearchQuery {
pub fn checked(
text: String,
sites: Vec<SearchSite>,
language: Option<String>,
limit: u32,
) -> Result<Self, SearchError> {
if text.trim().is_empty() {
return Err(SearchError::EmptyQuery);
}
if text.len() > 16_384 || sites.len() > 256 || limit == 0 || limit > 10_000 {
return Err(SearchError::BoundExceeded("query"));
}
Ok(Self {
text,
sites,
language,
limit,
})
}
pub fn to_datum(&self) -> Datum {
node(
"query",
vec![
field("text", Datum::String(self.text.clone())),
field(
"sites",
Datum::Vector(self.sites.iter().map(SearchSite::to_datum).collect()),
),
field(
"language",
self.language.clone().map_or(Datum::Nil, Datum::String),
),
field("limit", u32d(self.limit)),
],
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchSite {
pub domain: String,
pub include_subdomains: bool,
}
impl SearchSite {
fn to_datum(&self) -> Datum {
node(
"site",
vec![
field("domain", Datum::String(self.domain.clone())),
field("include-subdomains", Datum::Bool(self.include_subdomains)),
],
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProviderClaim {
pub provider: String,
pub uri: String,
pub title: Option<String>,
pub snippet: Option<String>,
pub position: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchObservation {
pub retrieval_uri: String,
pub claim: Option<ProviderClaim>,
pub capture_id: Option<ContentId>,
}
impl SearchObservation {
pub fn checked(
uri: &str,
claim: Option<ProviderClaim>,
capture_id: Option<ContentId>,
) -> Result<Self, SearchError> {
Ok(Self {
retrieval_uri: normalize_retrieval_uri(uri)
.map_err(|e| SearchError::Wire(e.to_string()))?
.as_str()
.to_owned(),
claim,
capture_id,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchPage {
pub query: SearchQuery,
pub observations: Vec<SearchObservation>,
pub continuation: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchNotice {
pub code: String,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AliasEvidence {
pub left_uri: String,
pub right_uri: String,
pub basis: String,
pub evidence_id: ContentId,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RankContribution {
pub observation: u32,
pub contributor: String,
pub score: NumberLiteral,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchRun {
pub query: SearchQuery,
pub pages: Vec<SearchPage>,
pub notices: Vec<SearchNotice>,
pub aliases: Vec<AliasEvidence>,
pub rank: Vec<RankContribution>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Citation {
pub selector: EvidenceSelector,
}
impl Citation {
pub fn checked(
rep: &WebRepresentation,
selector: EvidenceSelector,
) -> Result<Self, SearchError> {
selector.verify(rep)?;
Ok(Self { selector })
}
pub fn to_datum(&self) -> Datum {
node(
"citation",
vec![field("selector", self.selector.to_datum())],
)
}
pub fn from_datum(
value: &Datum,
rep: &WebRepresentation,
limits: DecodeLimits,
) -> Result<Self, SearchError> {
let Datum::Node { tag, fields } = value else {
return Err(SearchError::InvalidRecord("citation"));
};
if tag != &sym("citation") || fields.len() != 1 || fields[0].0 != sym("selector") {
return Err(SearchError::InvalidRecord("citation"));
}
Self::checked(
rep,
EvidenceSelector::from_datum(&fields[0].1, rep, limits)?,
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResearchBundle {
pub run: SearchRun,
pub representations: Vec<ContentId>,
pub citations: Vec<Citation>,
pub notices: Vec<SearchNotice>,
}
pub trait SearchWireCodec {
fn codec_id(&self) -> &str;
fn codec_version(&self) -> &str;
fn encode_request(
&self,
request: &SearchQuery,
limits: DecodeLimits,
) -> Result<Vec<u8>, SearchError>;
fn decode_config(&self, input: &[u8], limits: DecodeLimits) -> Result<Datum, SearchError>;
fn decode_response(
&self,
input: &[u8],
request: &SearchQuery,
limits: DecodeLimits,
) -> Result<SearchPage, SearchError>;
}
mod wire;
pub use wire::{RECORD_DESCRIPTORS, RecordDescriptor};
use wire::{field, node, sym, u32d};
#[cfg(test)]
mod tests;