1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum SearchType {
9 Auto,
11 #[default]
13 Neural,
14 Keyword,
16 Hybrid,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum LivecrawlOption {
24 Always,
26 Fallback,
28 Never,
30 Auto,
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct ContentsOptions {
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub text: Option<TextContentsOptions>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub highlights: Option<HighlightsContentsOptions>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub summary: Option<SummaryContentsOptions>,
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct TextContentsOptions {
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub max_characters: Option<u32>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub include_html_tags: Option<bool>,
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct HighlightsContentsOptions {
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub num_sentences: Option<u32>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub highlights_per_url: Option<u32>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub query: Option<String>,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct SummaryContentsOptions {
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub query: Option<String>,
83}
84
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct SearchResult {
89 pub url: String,
91 #[serde(default)]
93 pub id: Option<String>,
94 #[serde(default)]
96 pub title: Option<String>,
97 #[serde(default)]
99 pub score: Option<f64>,
100 #[serde(default)]
102 pub published_date: Option<String>,
103 #[serde(default)]
105 pub author: Option<String>,
106 #[serde(default)]
108 pub text: Option<String>,
109 #[serde(default)]
111 pub summary: Option<String>,
112 #[serde(default)]
114 pub highlights: Option<Vec<String>>,
115 #[serde(default)]
117 pub highlight_scores: Option<Vec<f64>>,
118}
119
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct CostDollarsSearch {
125 #[serde(default)]
127 pub neural: Option<f64>,
128 #[serde(default)]
130 pub keyword: Option<f64>,
131}
132
133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct CostDollarsContents {
138 #[serde(default)]
140 pub text: Option<f64>,
141 #[serde(default)]
143 pub highlights: Option<f64>,
144 #[serde(default)]
146 pub summary: Option<f64>,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct CostDollars {
153 #[serde(default)]
155 pub total: Option<f64>,
156 #[serde(default)]
158 pub search: Option<CostDollarsSearch>,
159 #[serde(default)]
161 pub contents: Option<CostDollarsContents>,
162}
163
164#[cfg(test)]
165mod cost_dollars_tests {
166 use super::*;
167 use serde_json::json;
168
169 #[test]
170 fn deserializes_full_breakdown() {
171 let v = json!({
172 "total": 0.005,
173 "search": { "neural": 0.003 },
174 "contents": { "text": 0.001, "highlights": 0.0005, "summary": 0.0005 }
175 });
176
177 let cost: CostDollars = serde_json::from_value(v).unwrap();
178
179 assert!((cost.total.unwrap() - 0.005).abs() < 1e-12);
180 assert!((cost.search.as_ref().unwrap().neural.unwrap() - 0.003).abs() < 1e-12);
181 assert!(cost.search.as_ref().unwrap().keyword.is_none());
182 assert!((cost.contents.as_ref().unwrap().text.unwrap() - 0.001).abs() < 1e-12);
183 assert!((cost.contents.as_ref().unwrap().highlights.unwrap() - 0.0005).abs() < 1e-12);
184 assert!((cost.contents.as_ref().unwrap().summary.unwrap() - 0.0005).abs() < 1e-12);
185 }
186
187 #[test]
188 fn deserializes_with_missing_optional_fields() {
189 let v = json!({
190 "total": 0.003,
191 "search": { "neural": 0.003 }
192 });
193
194 let cost: CostDollars = serde_json::from_value(v).unwrap();
195
196 assert!(cost.contents.is_none());
197 let search = cost.search.unwrap();
198 assert!((search.neural.unwrap() - 0.003).abs() < 1e-12);
199 assert!(search.keyword.is_none());
200 }
201
202 #[test]
203 fn deserializes_with_empty_nested_objects() {
204 let v = json!({
205 "total": 0.005,
206 "search": {},
207 "contents": {}
208 });
209
210 let cost: CostDollars = serde_json::from_value(v).unwrap();
211
212 assert!(cost.search.is_some());
213 assert!(cost.search.as_ref().unwrap().neural.is_none());
214 assert!(cost.contents.is_some());
215 assert!(cost.contents.as_ref().unwrap().text.is_none());
216 assert!(cost.contents.as_ref().unwrap().highlights.is_none());
217 assert!(cost.contents.as_ref().unwrap().summary.is_none());
218 }
219
220 #[test]
221 fn deserializes_with_null_nested_fields() {
222 let v = json!({
223 "total": 0.005,
224 "search": null,
225 "contents": { "text": null, "highlights": 0.0005 }
226 });
227
228 let cost: CostDollars = serde_json::from_value(v).unwrap();
229
230 assert!(cost.search.is_none());
231
232 let contents = cost.contents.unwrap();
233 assert!(contents.text.is_none());
234 assert!((contents.highlights.unwrap() - 0.0005).abs() < 1e-12);
235 assert!(contents.summary.is_none());
236 }
237
238 #[test]
239 fn ignores_unknown_fields_for_forward_compatibility() {
240 let v = json!({
241 "total": 0.005,
242 "search": { "neural": 0.003, "fast": 0.001 },
243 "contents": { "text": 0.002, "images": 0.123 },
244 "someFutureTopLevelField": "ignored"
245 });
246
247 let cost: CostDollars = serde_json::from_value(v).unwrap();
248
249 assert!((cost.search.as_ref().unwrap().neural.unwrap() - 0.003).abs() < 1e-12);
250 assert!(cost.search.as_ref().unwrap().keyword.is_none());
251 assert!((cost.contents.as_ref().unwrap().text.unwrap() - 0.002).abs() < 1e-12);
252 }
253}