1#![forbid(unsafe_code)]
8
9use sim_kernel::{ContentId, Datum, NumberLiteral};
10use sim_lib_net_core::normalize_retrieval_uri;
11use sim_lib_web_core::{DecodeLimits, EvidenceSelector, WebRecordError, WebRepresentation};
12use std::{error::Error, fmt};
13
14pub static RECIPES: sim_cookbook::EmbeddedDir =
16 include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum SearchError {
20 EmptyQuery,
21 BoundExceeded(&'static str),
22 InvalidRecord(&'static str),
23 Citation(WebRecordError),
24 Wire(String),
25}
26impl fmt::Display for SearchError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(f, "{self:?}")
29 }
30}
31impl Error for SearchError {}
32impl From<WebRecordError> for SearchError {
33 fn from(value: WebRecordError) -> Self {
34 Self::Citation(value)
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct SearchQuery {
40 pub text: String,
41 pub sites: Vec<SearchSite>,
42 pub language: Option<String>,
43 pub limit: u32,
44}
45impl SearchQuery {
46 pub fn checked(
47 text: String,
48 sites: Vec<SearchSite>,
49 language: Option<String>,
50 limit: u32,
51 ) -> Result<Self, SearchError> {
52 if text.trim().is_empty() {
53 return Err(SearchError::EmptyQuery);
54 }
55 if text.len() > 16_384 || sites.len() > 256 || limit == 0 || limit > 10_000 {
56 return Err(SearchError::BoundExceeded("query"));
57 }
58 Ok(Self {
59 text,
60 sites,
61 language,
62 limit,
63 })
64 }
65 pub fn to_datum(&self) -> Datum {
66 node(
67 "query",
68 vec![
69 field("text", Datum::String(self.text.clone())),
70 field(
71 "sites",
72 Datum::Vector(self.sites.iter().map(SearchSite::to_datum).collect()),
73 ),
74 field(
75 "language",
76 self.language.clone().map_or(Datum::Nil, Datum::String),
77 ),
78 field("limit", u32d(self.limit)),
79 ],
80 )
81 }
82}
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct SearchSite {
85 pub domain: String,
86 pub include_subdomains: bool,
87}
88impl SearchSite {
89 fn to_datum(&self) -> Datum {
90 node(
91 "site",
92 vec![
93 field("domain", Datum::String(self.domain.clone())),
94 field("include-subdomains", Datum::Bool(self.include_subdomains)),
95 ],
96 )
97 }
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct ProviderClaim {
103 pub provider: String,
104 pub uri: String,
105 pub title: Option<String>,
106 pub snippet: Option<String>,
107 pub position: Option<u32>,
108}
109#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct SearchObservation {
112 pub retrieval_uri: String,
113 pub claim: Option<ProviderClaim>,
114 pub capture_id: Option<ContentId>,
115}
116impl SearchObservation {
117 pub fn checked(
118 uri: &str,
119 claim: Option<ProviderClaim>,
120 capture_id: Option<ContentId>,
121 ) -> Result<Self, SearchError> {
122 Ok(Self {
123 retrieval_uri: normalize_retrieval_uri(uri)
124 .map_err(|e| SearchError::Wire(e.to_string()))?
125 .as_str()
126 .to_owned(),
127 claim,
128 capture_id,
129 })
130 }
131}
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct SearchPage {
134 pub query: SearchQuery,
135 pub observations: Vec<SearchObservation>,
136 pub continuation: Option<String>,
137}
138#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct SearchNotice {
140 pub code: String,
141 pub message: String,
142}
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct AliasEvidence {
145 pub left_uri: String,
146 pub right_uri: String,
147 pub basis: String,
148 pub evidence_id: ContentId,
149}
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct RankContribution {
152 pub observation: u32,
153 pub contributor: String,
154 pub score: NumberLiteral,
155 pub reason: String,
156}
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct SearchRun {
159 pub query: SearchQuery,
160 pub pages: Vec<SearchPage>,
161 pub notices: Vec<SearchNotice>,
162 pub aliases: Vec<AliasEvidence>,
163 pub rank: Vec<RankContribution>,
164}
165
166#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct Citation {
169 pub selector: EvidenceSelector,
170}
171impl Citation {
172 pub fn checked(
173 rep: &WebRepresentation,
174 selector: EvidenceSelector,
175 ) -> Result<Self, SearchError> {
176 selector.verify(rep)?;
177 Ok(Self { selector })
178 }
179 pub fn to_datum(&self) -> Datum {
180 node(
181 "citation",
182 vec![field("selector", self.selector.to_datum())],
183 )
184 }
185 pub fn from_datum(
186 value: &Datum,
187 rep: &WebRepresentation,
188 limits: DecodeLimits,
189 ) -> Result<Self, SearchError> {
190 let Datum::Node { tag, fields } = value else {
191 return Err(SearchError::InvalidRecord("citation"));
192 };
193 if tag != &sym("citation") || fields.len() != 1 || fields[0].0 != sym("selector") {
194 return Err(SearchError::InvalidRecord("citation"));
195 }
196 Self::checked(
197 rep,
198 EvidenceSelector::from_datum(&fields[0].1, rep, limits)?,
199 )
200 }
201}
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct ResearchBundle {
204 pub run: SearchRun,
205 pub representations: Vec<ContentId>,
206 pub citations: Vec<Citation>,
207 pub notices: Vec<SearchNotice>,
208}
209
210pub trait SearchWireCodec {
212 fn codec_id(&self) -> &str;
213 fn codec_version(&self) -> &str;
214 fn encode_request(
215 &self,
216 request: &SearchQuery,
217 limits: DecodeLimits,
218 ) -> Result<Vec<u8>, SearchError>;
219 fn decode_config(&self, input: &[u8], limits: DecodeLimits) -> Result<Datum, SearchError>;
220 fn decode_response(
221 &self,
222 input: &[u8],
223 request: &SearchQuery,
224 limits: DecodeLimits,
225 ) -> Result<SearchPage, SearchError>;
226}
227
228mod wire;
229
230pub use wire::{RECORD_DESCRIPTORS, RecordDescriptor};
231use wire::{field, node, sym, u32d};
232
233#[cfg(test)]
234mod tests;