google_search_console_api/search_analytics/
query.rs1use crate::types::{Dimension, DimensionFilterGroup};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
10#[serde(rename_all = "camelCase")]
11pub enum SearchType {
12 #[default]
14 Web,
15 Image,
17 Video,
19 News,
21 Discover,
23 GoogleNews,
25}
26
27#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
31#[serde(rename_all = "camelCase")]
32pub enum AggregationType {
33 #[default]
35 Auto,
36 ByPage,
38 ByProperty,
40}
41
42#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
46#[serde(rename_all = "camelCase")]
47pub enum DataState {
48 #[default]
50 All,
51 Final,
53}
54
55#[derive(Default, Debug, Serialize, Deserialize, Clone)]
71pub struct SearchAnalyticsQueryRequest {
72 #[serde(rename = "startDate")]
74 pub start_date: String,
75 #[serde(rename = "endDate")]
77 pub end_date: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub dimensions: Option<Vec<Dimension>>,
81 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
83 pub search_type: Option<SearchType>,
84 #[serde(
86 rename = "dimensionFilterGroups",
87 skip_serializing_if = "Option::is_none"
88 )]
89 pub dimension_filter_groups: Option<Vec<DimensionFilterGroup>>,
90 #[serde(rename = "aggregationType", skip_serializing_if = "Option::is_none")]
92 pub aggregation_type: Option<AggregationType>,
93 #[serde(rename = "rowLimit", skip_serializing_if = "Option::is_none")]
95 pub row_limit: Option<usize>,
96 #[serde(rename = "startRow", skip_serializing_if = "Option::is_none")]
98 pub start_row: Option<usize>,
99 #[serde(rename = "dataState", skip_serializing_if = "Option::is_none")]
101 pub data_state: Option<DataState>,
102}
103
104impl SearchAnalyticsQueryRequest {
105 pub fn builder(start_date: &str, end_date: &str) -> SearchAnalyticsQueryRequestBuilder {
122 SearchAnalyticsQueryRequestBuilder {
123 start_date: start_date.to_string(),
124 end_date: end_date.to_string(),
125 ..Default::default()
126 }
127 }
128}
129
130#[derive(Default, Debug, Clone)]
134pub struct SearchAnalyticsQueryRequestBuilder {
135 start_date: String,
136 end_date: String,
137 dimensions: Option<Vec<Dimension>>,
138 search_type: Option<SearchType>,
139 dimension_filter_groups: Option<Vec<DimensionFilterGroup>>,
140 aggregation_type: Option<AggregationType>,
141 row_limit: Option<usize>,
142 start_row: Option<usize>,
143 data_state: Option<DataState>,
144}
145
146impl SearchAnalyticsQueryRequestBuilder {
147 pub fn dimensions(mut self, dimensions: Vec<Dimension>) -> Self {
151 self.dimensions = Some(dimensions);
152 self
153 }
154
155 pub fn search_type(mut self, search_type: SearchType) -> Self {
159 self.search_type = Some(search_type);
160 self
161 }
162
163 pub fn dimension_filter_groups(mut self, groups: Vec<DimensionFilterGroup>) -> Self {
167 self.dimension_filter_groups = Some(groups);
168 self
169 }
170
171 pub fn aggregation_type(mut self, aggregation_type: AggregationType) -> Self {
175 self.aggregation_type = Some(aggregation_type);
176 self
177 }
178
179 pub fn row_limit(mut self, limit: usize) -> Self {
183 self.row_limit = Some(limit.min(25000));
184 self
185 }
186
187 pub fn start_row(mut self, start: usize) -> Self {
191 self.start_row = Some(start);
192 self
193 }
194
195 pub fn data_state(mut self, data_state: DataState) -> Self {
199 self.data_state = Some(data_state);
200 self
201 }
202
203 pub fn build(self) -> SearchAnalyticsQueryRequest {
207 SearchAnalyticsQueryRequest {
208 start_date: self.start_date,
209 end_date: self.end_date,
210 dimensions: self.dimensions,
211 search_type: self.search_type,
212 dimension_filter_groups: self.dimension_filter_groups,
213 aggregation_type: self.aggregation_type,
214 row_limit: self.row_limit,
215 start_row: self.start_row,
216 data_state: self.data_state,
217 }
218 }
219}
220
221#[derive(Default, Debug, Serialize, Deserialize, Clone)]
223pub struct SearchAnalyticsQueryResponse {
224 pub rows: Option<Vec<SearchAnalyticsQueryResponseRow>>,
226 #[serde(rename = "responseAggregationType")]
228 pub response_aggregation_type: Option<String>,
229}
230
231#[derive(Default, Debug, Serialize, Deserialize, Clone)]
233pub struct SearchAnalyticsQueryResponseRow {
234 pub keys: Option<Vec<String>>,
236 pub clicks: Option<f32>,
238 pub impressions: Option<f32>,
240 pub ctr: Option<f32>,
242 pub position: Option<f32>,
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_builder_basic() {
252 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31").build();
253
254 assert_eq!(request.start_date, "2024-01-01");
255 assert_eq!(request.end_date, "2024-01-31");
256 assert!(request.dimensions.is_none());
257 assert!(request.row_limit.is_none());
258 }
259
260 #[test]
261 fn test_builder_with_dimensions() {
262 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31")
263 .dimensions(vec![Dimension::Query, Dimension::Page])
264 .build();
265
266 let dims = request.dimensions.unwrap();
267 assert_eq!(dims.len(), 2);
268 assert_eq!(dims[0], Dimension::Query);
269 assert_eq!(dims[1], Dimension::Page);
270 }
271
272 #[test]
273 fn test_builder_row_limit_clamped() {
274 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31")
275 .row_limit(50000)
276 .build();
277
278 assert_eq!(request.row_limit, Some(25000));
279 }
280
281 #[test]
282 fn test_builder_row_limit_normal() {
283 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31")
284 .row_limit(100)
285 .build();
286
287 assert_eq!(request.row_limit, Some(100));
288 }
289
290 #[test]
291 fn test_builder_all_options() {
292 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31")
293 .dimensions(vec![Dimension::Query])
294 .search_type(SearchType::Web)
295 .aggregation_type(AggregationType::ByPage)
296 .data_state(DataState::Final)
297 .row_limit(1000)
298 .start_row(100)
299 .build();
300
301 assert_eq!(request.start_date, "2024-01-01");
302 assert_eq!(request.end_date, "2024-01-31");
303 assert_eq!(request.dimensions.unwrap().len(), 1);
304 assert_eq!(request.search_type, Some(SearchType::Web));
305 assert_eq!(request.aggregation_type, Some(AggregationType::ByPage));
306 assert_eq!(request.data_state, Some(DataState::Final));
307 assert_eq!(request.row_limit, Some(1000));
308 assert_eq!(request.start_row, Some(100));
309 }
310
311 #[test]
312 fn test_search_type_serialize() {
313 assert_eq!(serde_json::to_string(&SearchType::Web).unwrap(), "\"web\"");
314 assert_eq!(
315 serde_json::to_string(&SearchType::Image).unwrap(),
316 "\"image\""
317 );
318 assert_eq!(
319 serde_json::to_string(&SearchType::Discover).unwrap(),
320 "\"discover\""
321 );
322 assert_eq!(
323 serde_json::to_string(&SearchType::GoogleNews).unwrap(),
324 "\"googleNews\""
325 );
326 }
327
328 #[test]
329 fn test_aggregation_type_serialize() {
330 assert_eq!(
331 serde_json::to_string(&AggregationType::Auto).unwrap(),
332 "\"auto\""
333 );
334 assert_eq!(
335 serde_json::to_string(&AggregationType::ByPage).unwrap(),
336 "\"byPage\""
337 );
338 assert_eq!(
339 serde_json::to_string(&AggregationType::ByProperty).unwrap(),
340 "\"byProperty\""
341 );
342 }
343
344 #[test]
345 fn test_data_state_serialize() {
346 assert_eq!(serde_json::to_string(&DataState::All).unwrap(), "\"all\"");
347 assert_eq!(
348 serde_json::to_string(&DataState::Final).unwrap(),
349 "\"final\""
350 );
351 }
352
353 #[test]
354 fn test_request_serialize() {
355 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31")
356 .dimensions(vec![Dimension::Query])
357 .row_limit(100)
358 .build();
359
360 let json = serde_json::to_string(&request).unwrap();
361 assert!(json.contains("\"startDate\":\"2024-01-01\""));
362 assert!(json.contains("\"endDate\":\"2024-01-31\""));
363 assert!(json.contains("\"dimensions\":[\"query\"]"));
364 assert!(json.contains("\"rowLimit\":100"));
365 }
366
367 #[test]
368 fn test_request_skip_none_fields() {
369 let request = SearchAnalyticsQueryRequest::builder("2024-01-01", "2024-01-31").build();
370
371 let json = serde_json::to_string(&request).unwrap();
372 assert!(!json.contains("dimensions"));
373 assert!(!json.contains("rowLimit"));
374 assert!(!json.contains("type"));
375 }
376
377 #[test]
378 fn test_response_deserialize() {
379 let json = r#"{
380 "rows": [
381 {
382 "keys": ["test query"],
383 "clicks": 100.0,
384 "impressions": 1000.0,
385 "ctr": 0.1,
386 "position": 5.5
387 }
388 ],
389 "responseAggregationType": "auto"
390 }"#;
391
392 let response: SearchAnalyticsQueryResponse = serde_json::from_str(json).unwrap();
393 assert!(response.rows.is_some());
394
395 let rows = response.rows.unwrap();
396 assert_eq!(rows.len(), 1);
397 assert_eq!(rows[0].clicks, Some(100.0));
398 assert_eq!(rows[0].impressions, Some(1000.0));
399 assert_eq!(rows[0].ctr, Some(0.1));
400 assert_eq!(rows[0].position, Some(5.5));
401 }
402
403 #[test]
404 fn test_response_empty_rows() {
405 let json = r#"{}"#;
406
407 let response: SearchAnalyticsQueryResponse = serde_json::from_str(json).unwrap();
408 assert!(response.rows.is_none());
409 }
410}