1use serde::Deserialize;
4use serde::Serialize;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "lowercase")]
9pub enum SearchType {
10 Auto,
12 #[default]
14 Neural,
15 Keyword,
17 Hybrid,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(rename_all = "lowercase")]
24pub enum LivecrawlOption {
25 Always,
27 Fallback,
29 Never,
31 Auto,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ContentsOptions {
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub text: Option<TextContentsOptions>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub highlights: Option<HighlightsContentsOptions>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub summary: Option<SummaryContentsOptions>,
48}
49
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct TextContentsOptions {
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub max_characters: Option<u32>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub include_html_tags: Option<bool>,
60}
61
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct HighlightsContentsOptions {
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub num_sentences: Option<u32>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub highlights_per_url: Option<u32>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub query: Option<String>,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct SummaryContentsOptions {
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub query: Option<String>,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct SearchResult {
90 pub url: String,
92 #[serde(default)]
94 pub id: Option<String>,
95 #[serde(default)]
97 pub title: Option<String>,
98 #[serde(default)]
100 pub score: Option<f64>,
101 #[serde(default)]
103 pub published_date: Option<String>,
104 #[serde(default)]
106 pub author: Option<String>,
107 #[serde(default)]
109 pub text: Option<String>,
110 #[serde(default)]
112 pub summary: Option<String>,
113 #[serde(default)]
115 pub highlights: Option<Vec<String>>,
116 #[serde(default)]
118 pub highlight_scores: Option<Vec<f64>>,
119}
120
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct CostDollarsSearch {
126 #[serde(default)]
128 pub neural: Option<f64>,
129 #[serde(default)]
131 pub keyword: Option<f64>,
132}
133
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct CostDollarsContents {
139 #[serde(default)]
141 pub text: Option<f64>,
142 #[serde(default)]
144 pub highlights: Option<f64>,
145 #[serde(default)]
147 pub summary: Option<f64>,
148}
149
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct CostDollars {
154 #[serde(default)]
156 pub total: Option<f64>,
157 #[serde(default)]
159 pub search: Option<CostDollarsSearch>,
160 #[serde(default)]
162 pub contents: Option<CostDollarsContents>,
163}
164
165#[cfg(test)]
166mod cost_dollars_tests {
167 use super::*;
168 use serde_json::json;
169
170 #[test]
171 fn deserializes_full_breakdown() {
172 let v = json!({
173 "total": 0.005,
174 "search": { "neural": 0.003 },
175 "contents": { "text": 0.001, "highlights": 0.0005, "summary": 0.0005 }
176 });
177
178 let cost: CostDollars = serde_json::from_value(v).unwrap();
179
180 assert!((cost.total.unwrap() - 0.005).abs() < 1e-12);
181 assert!((cost.search.as_ref().unwrap().neural.unwrap() - 0.003).abs() < 1e-12);
182 assert!(cost.search.as_ref().unwrap().keyword.is_none());
183 assert!((cost.contents.as_ref().unwrap().text.unwrap() - 0.001).abs() < 1e-12);
184 assert!((cost.contents.as_ref().unwrap().highlights.unwrap() - 0.0005).abs() < 1e-12);
185 assert!((cost.contents.as_ref().unwrap().summary.unwrap() - 0.0005).abs() < 1e-12);
186 }
187
188 #[test]
189 fn deserializes_with_missing_optional_fields() {
190 let v = json!({
191 "total": 0.003,
192 "search": { "neural": 0.003 }
193 });
194
195 let cost: CostDollars = serde_json::from_value(v).unwrap();
196
197 assert!(cost.contents.is_none());
198 let search = cost.search.unwrap();
199 assert!((search.neural.unwrap() - 0.003).abs() < 1e-12);
200 assert!(search.keyword.is_none());
201 }
202
203 #[test]
204 fn deserializes_with_empty_nested_objects() {
205 let v = json!({
206 "total": 0.005,
207 "search": {},
208 "contents": {}
209 });
210
211 let cost: CostDollars = serde_json::from_value(v).unwrap();
212
213 assert!(cost.search.is_some());
214 assert!(cost.search.as_ref().unwrap().neural.is_none());
215 assert!(cost.contents.is_some());
216 assert!(cost.contents.as_ref().unwrap().text.is_none());
217 assert!(cost.contents.as_ref().unwrap().highlights.is_none());
218 assert!(cost.contents.as_ref().unwrap().summary.is_none());
219 }
220
221 #[test]
222 fn deserializes_with_null_nested_fields() {
223 let v = json!({
224 "total": 0.005,
225 "search": null,
226 "contents": { "text": null, "highlights": 0.0005 }
227 });
228
229 let cost: CostDollars = serde_json::from_value(v).unwrap();
230
231 assert!(cost.search.is_none());
232
233 let contents = cost.contents.unwrap();
234 assert!(contents.text.is_none());
235 assert!((contents.highlights.unwrap() - 0.0005).abs() < 1e-12);
236 assert!(contents.summary.is_none());
237 }
238
239 #[test]
240 fn ignores_unknown_fields_for_forward_compatibility() {
241 let v = json!({
242 "total": 0.005,
243 "search": { "neural": 0.003, "fast": 0.001 },
244 "contents": { "text": 0.002, "images": 0.123 },
245 "someFutureTopLevelField": "ignored"
246 });
247
248 let cost: CostDollars = serde_json::from_value(v).unwrap();
249
250 assert!((cost.search.as_ref().unwrap().neural.unwrap() - 0.003).abs() < 1e-12);
251 assert!(cost.search.as_ref().unwrap().keyword.is_none());
252 assert!((cost.contents.as_ref().unwrap().text.unwrap() - 0.002).abs() < 1e-12);
253 }
254}