algoliasearch/index/
mod.rs

1use std::{fmt, marker::PhantomData};
2
3use chrono::{DateTime, Utc};
4
5use reqwest::{header::HeaderMap, Client};
6use serde::{
7    de::{self, Deserialize, DeserializeOwned, Deserializer, Visitor},
8    ser::{Serialize, Serializer},
9};
10
11use crate::error::Error;
12
13pub mod settings;
14pub mod task;
15
16#[derive(Debug, Deserialize)]
17#[serde(rename_all = "camelCase")]
18/// Search result
19pub struct SearchResult<T> {
20    /// Hits
21    pub hits: Vec<T>,
22    /// Number of hits
23    pub nb_hits: u64,
24    /// Page
25    pub page: u64,
26    /// Number of pages
27    pub nb_pages: u64,
28    /// Number of hits per page
29    pub hits_per_page: u64,
30    #[serde(rename = "processingTimeMS")]
31    /// Processing time (ms)
32    pub processing_time_ms: u64,
33    /// Is the search exhaustive?
34    pub exhaustive_nb_hits: bool,
35    /// Query
36    pub query: String,
37    /// Params
38    pub params: String,
39}
40
41#[derive(Clone, Debug, PartialEq, Hash)]
42/// [https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/](https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/)
43pub enum AroundRadius {
44    #[allow(missing_docs)]
45    All,
46    #[allow(missing_docs)]
47    Radius(u64),
48}
49
50struct AroundRadiusVisitor;
51
52impl<'de> Visitor<'de> for AroundRadiusVisitor {
53    type Value = AroundRadius;
54
55    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
56        formatter.write_str("all or radius in meters")
57    }
58
59    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
60    where
61        E: de::Error,
62    {
63        if value == "all" {
64            Ok(AroundRadius::All)
65        } else {
66            Err(E::custom(format!(r#"expected "all", got "{}""#, value)))
67        }
68    }
69
70    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
71where {
72        Ok(AroundRadius::Radius(value))
73    }
74}
75
76impl<'de> Deserialize<'de> for AroundRadius {
77    fn deserialize<D>(deserializer: D) -> Result<AroundRadius, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        deserializer.deserialize_any(AroundRadiusVisitor)
82    }
83}
84
85impl Serialize for AroundRadius {
86    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: Serializer,
89    {
90        match self {
91            AroundRadius::All => serializer.serialize_str("all"),
92            AroundRadius::Radius(v) => serializer.serialize_u64(*v),
93        }
94    }
95}
96
97#[cfg(test)]
98mod ignore_plurals_tests {
99    use super::*;
100    use serde_json;
101
102    #[test]
103    fn test_serialize() {
104        assert_eq!(
105            serde_json::to_string(&AroundRadius::All).unwrap(),
106            r#""all""#
107        );
108        assert_eq!(
109            serde_json::to_string(&AroundRadius::Radius(20)).unwrap(),
110            r#"20"#
111        );
112    }
113
114    #[test]
115    fn test_deserialize() {
116        assert_eq!(
117            serde_json::from_str::<AroundRadius>(r#""all""#).unwrap(),
118            AroundRadius::All
119        );
120        assert_eq!(
121            serde_json::from_str::<AroundRadius>(r#"20"#).unwrap(),
122            AroundRadius::Radius(20)
123        );
124        assert_eq!(
125            serde_json::from_str::<AroundRadius>(r#""unknown""#)
126                .unwrap_err()
127                .to_string(),
128            "expected \"all\", got \"unknown\" at line 1 column 9"
129        );
130    }
131}
132
133#[derive(Clone, Debug, Serialize, Deserialize)]
134pub enum StringOrVecOfString {
135    String(String),
136    VecOfString(Vec<String>),
137}
138
139#[derive(Debug, Serialize, Default, Builder)]
140#[serde(rename_all = "camelCase")]
141#[builder(default)]
142/// algolia search parameters
143/// see [https://www.algolia.com/doc/api-reference/search-api-parameters/](https://www.algolia.com/doc/api-reference/search-api-parameters/)
144pub struct SearchQuery {
145    // search
146    #[builder(setter(into))]
147    /// [https://www.algolia.com/doc/api-reference/api-parameters/query/](https://www.algolia.com/doc/api-reference/api-parameters/query/)
148    query: Option<String>,
149
150    // attributes
151    #[builder(setter(into))]
152    #[serde(skip_serializing_if = "Option::is_none")]
153    /// [https://www.algolia.com/doc/api-reference/api-parameters/attributesToRetrieve/](https://www.algolia.com/doc/api-reference/api-parameters/attributesToRetrieve/)
154    attributes_to_retrieve: Option<Vec<String>>,
155    #[builder(setter(into))]
156    #[serde(skip_serializing_if = "Option::is_none")]
157    /// [](https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/)
158    restrict_searchable_attributes: Option<Vec<String>>,
159
160    // filtering
161    #[builder(setter(into))]
162    #[serde(skip_serializing_if = "Option::is_none")]
163    /// [https://www.algolia.com/doc/api-reference/api-parameters/filters/](https://www.algolia.com/doc/api-reference/api-parameters/filters/)
164    filters: Option<String>,
165    #[builder(setter(into))]
166    #[serde(skip_serializing_if = "Option::is_none")]
167    /// [https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/](https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/)
168    facet_filters: Option<Vec<StringOrVecOfString>>,
169    #[builder(setter(into))]
170    #[serde(skip_serializing_if = "Option::is_none")]
171    /// [https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/](https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/)
172    optional_filters: Option<Vec<StringOrVecOfString>>,
173    #[builder(setter(into))]
174    #[serde(skip_serializing_if = "Option::is_none")]
175    /// [https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/](https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/)
176    numeric_filters: Option<Vec<String>>,
177    #[builder(setter(into))]
178    #[serde(skip_serializing_if = "Option::is_none")]
179    /// [https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/](https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/)
180    tag_filters: Option<StringOrVecOfString>,
181    #[builder(setter(into))]
182    #[serde(skip_serializing_if = "Option::is_none")]
183    /// [https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/](https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/)
184    sum_or_filters_scores: Option<bool>,
185
186    // faceting
187    #[builder(setter(into))]
188    #[serde(skip_serializing_if = "Option::is_none")]
189    /// [https://www.algolia.com/doc/api-reference/api-parameters/facets/](https://www.algolia.com/doc/api-reference/api-parameters/facets/)
190    facets: Option<Vec<String>>,
191    #[builder(setter(into))]
192    #[serde(skip_serializing_if = "Option::is_none")]
193    /// [https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/](https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/)
194    max_values_per_facet: Option<u64>,
195    #[builder(setter(into))]
196    #[serde(skip_serializing_if = "Option::is_none")]
197    /// [https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/](https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/)
198    faceting_after_distinct: Option<bool>,
199    #[builder(setter(into))]
200    #[serde(skip_serializing_if = "Option::is_none")]
201    /// [https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/](https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/)
202    sort_facet_values_by: Option<crate::settings::SortFacetValuesBy>,
203
204    // highlighting-snippeting
205    #[builder(setter(into))]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    /// [https://www.algolia.com/doc/api-reference/api-parameters/attributesToHighlight/](https://www.algolia.com/doc/api-reference/api-parameters/attributesToHighlight/)
208    attributes_to_highlight: Option<Vec<String>>,
209    #[builder(setter(into))]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    /// [https://www.algolia.com/doc/api-reference/api-parameters/attributesToSnippet/](https://www.algolia.com/doc/api-reference/api-parameters/attributesToSnippet/)
212    attributes_to_snippet: Option<Vec<String>>,
213    #[builder(setter(into))]
214    #[serde(skip_serializing_if = "Option::is_none")]
215    /// [https://www.algolia.com/doc/api-reference/api-parameters/highlightPreTag/](https://www.algolia.com/doc/api-reference/api-parameters/highlightPreTag/)
216    highlight_pre_tag: Option<String>,
217    #[builder(setter(into))]
218    #[serde(skip_serializing_if = "Option::is_none")]
219    /// [https://www.algolia.com/doc/api-reference/api-parameters/highlightPostTag/](https://www.algolia.com/doc/api-reference/api-parameters/highlightPostTag/)
220    highlight_post_tag: Option<String>,
221    #[builder(setter(into))]
222    #[serde(skip_serializing_if = "Option::is_none")]
223    /// [https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisText/](https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisText/)
224    snippet_ellipsis_text: Option<String>,
225    #[builder(setter(into))]
226    #[serde(skip_serializing_if = "Option::is_none")]
227    /// [https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/](https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/)
228    restrict_highlight_and_snippet_arrays: Option<bool>,
229
230    // pagination
231    #[builder(setter(into))]
232    #[serde(skip_serializing_if = "Option::is_none")]
233    /// [https://www.algolia.com/doc/api-reference/api-parameters/page/](https://www.algolia.com/doc/api-reference/api-parameters/page/)
234    page: Option<u64>,
235    #[builder(setter(into))]
236    #[serde(skip_serializing_if = "Option::is_none")]
237    /// [https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/](https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/)
238    hits_per_page: Option<u64>,
239    #[builder(setter(into))]
240    #[serde(skip_serializing_if = "Option::is_none")]
241    /// [https://www.algolia.com/doc/api-reference/api-parameters/offset/](https://www.algolia.com/doc/api-reference/api-parameters/offset/)
242    offset: Option<u64>,
243    #[builder(setter(into))]
244    #[serde(skip_serializing_if = "Option::is_none")]
245    /// [https://www.algolia.com/doc/api-reference/api-parameters/length/](https://www.algolia.com/doc/api-reference/api-parameters/length/)
246    length: Option<u64>,
247
248    // typos
249    #[builder(setter(into))]
250    #[serde(skip_serializing_if = "Option::is_none")]
251    #[serde(rename = "minWordSizefor1Typo")]
252    /// [https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor1Typo/](https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor1Typo/)
253    min_word_sizefor_1_typo: Option<u64>,
254    #[builder(setter(into))]
255    #[serde(skip_serializing_if = "Option::is_none")]
256    #[serde(rename = "minWordSizefor2Typo")]
257    /// [https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor2Typos/](https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor2Typos/)
258    min_word_sizefor_2_typos: Option<u64>,
259    #[builder(setter(into))]
260    #[serde(skip_serializing_if = "Option::is_none")]
261    /// [https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/](https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/)
262    typo_tolerance: Option<crate::settings::TypoTolerance>,
263    #[builder(setter(into))]
264    #[serde(skip_serializing_if = "Option::is_none")]
265    /// [https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/](https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/)
266    allow_typos_on_numeric_tokens: Option<bool>,
267    #[builder(setter(into))]
268    #[serde(skip_serializing_if = "Option::is_none")]
269    /// [https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleranceOnAttributes/](https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleranceOnAttributes/)
270    disable_typo_tolerance_on_attributes: Option<Vec<String>>,
271
272    // geo-search
273    #[builder(setter(into))]
274    #[serde(skip_serializing_if = "Option::is_none")]
275    /// [https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/)
276    around_lat_lng: Option<String>,
277    #[builder(setter(into))]
278    #[serde(skip_serializing_if = "Option::is_none")]
279    #[serde(rename = "aroundLatLngViaIP")]
280    /// [https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/)
281    around_lat_lng_via_ip: Option<bool>,
282    #[builder(setter(into))]
283    #[serde(skip_serializing_if = "Option::is_none")]
284    /// [https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/](https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/)
285    around_radius: Option<AroundRadius>,
286    #[builder(setter(into))]
287    #[serde(skip_serializing_if = "Option::is_none")]
288    /// [https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/](https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/)
289    around_precision: Option<u64>,
290    #[builder(setter(into))]
291    #[serde(skip_serializing_if = "Option::is_none")]
292    /// [https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/](https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/)
293    minimum_around_radius: Option<u64>,
294    #[builder(setter(into))]
295    #[serde(skip_serializing_if = "Option::is_none")]
296    /// [https://www.algolia.com/doc/api-reference/api-parameters/insideBoundingBox/](https://www.algolia.com/doc/api-reference/api-parameters/insideBoundingBox/)
297    inside_bounding_box: Option<Vec<f64>>,
298    #[builder(setter(into))]
299    #[serde(skip_serializing_if = "Option::is_none")]
300    /// [https://www.algolia.com/doc/api-reference/api-parameters/insidePolygon/](https://www.algolia.com/doc/api-reference/api-parameters/insidePolygon/)
301    inside_polygon: Option<Vec<f64>>,
302
303    // languages
304    #[builder(setter(into))]
305    #[serde(skip_serializing_if = "Option::is_none")]
306    /// [https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/](https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/)
307    ignore_plurals: Option<crate::settings::IgnorePlurals>,
308    #[builder(setter(into))]
309    #[serde(skip_serializing_if = "Option::is_none")]
310    /// [https://www.algolia.com/doc/api-reference/api-parameters/removeStopWords/](https://www.algolia.com/doc/api-reference/api-parameters/removeStopWords/)
311    remove_stop_words: Option<crate::settings::IgnorePlurals>,
312    #[builder(setter(into))]
313    #[serde(skip_serializing_if = "Option::is_none")]
314    /// [https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/](https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/)
315    query_languages: Option<Vec<String>>,
316
317    // query-strategy
318    #[builder(setter(into))]
319    #[serde(skip_serializing_if = "Option::is_none")]
320    /// [https://www.algolia.com/doc/api-reference/api-parameters/queryType/](https://www.algolia.com/doc/api-reference/api-parameters/queryType/)
321    query_type: Option<crate::settings::QueryType>,
322    #[builder(setter(into))]
323    #[serde(skip_serializing_if = "Option::is_none")]
324    /// [https://www.algolia.com/doc/api-reference/api-parameters/removeWordsIfNoResults/](https://www.algolia.com/doc/api-reference/api-parameters/removeWordsIfNoResults/)
325    remove_words_if_no_results: Option<crate::settings::RemoveWordsIfNoResults>,
326    #[builder(setter(into))]
327    #[serde(skip_serializing_if = "Option::is_none")]
328    /// [https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntax/](https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntax/)
329    advanced_syntax: Option<bool>,
330    #[builder(setter(into))]
331    #[serde(skip_serializing_if = "Option::is_none")]
332    /// [https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/](https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/)
333    optional_words: Option<Vec<String>>,
334    #[builder(setter(into))]
335    #[serde(skip_serializing_if = "Option::is_none")]
336    /// [https://www.algolia.com/doc/api-reference/api-parameters/disableExactOnAttributes/](https://www.algolia.com/doc/api-reference/api-parameters/disableExactOnAttributes/)
337    disable_exact_on_attributes: Option<Vec<String>>,
338    #[builder(setter(into))]
339    #[serde(skip_serializing_if = "Option::is_none")]
340    /// [https://www.algolia.com/doc/api-reference/api-parameters/exactOnSingleWordQuery/](https://www.algolia.com/doc/api-reference/api-parameters/exactOnSingleWordQuery/)
341    exact_on_single_word_query: Option<crate::settings::ExactOnSingleWordQuery>,
342    #[builder(setter(into))]
343    #[serde(skip_serializing_if = "Option::is_none")]
344    /// [https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/](https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/)
345    alternatives_as_exact: Option<crate::settings::AlternativesAsExact>,
346
347    // query-rules
348    #[builder(setter(into))]
349    #[serde(skip_serializing_if = "Option::is_none")]
350    /// [https://www.algolia.com/doc/api-reference/api-parameters/enableRules/](https://www.algolia.com/doc/api-reference/api-parameters/enableRules/)
351    enable_rules: Option<bool>,
352    #[builder(setter(into))]
353    #[serde(skip_serializing_if = "Option::is_none")]
354    /// [https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/](https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/)
355    rule_contexts: Option<Vec<String>>,
356
357    // personalization
358    #[builder(setter(into))]
359    #[serde(skip_serializing_if = "Option::is_none")]
360    /// [https://www.algolia.com/doc/api-reference/api-parameters/enablePersonalization/](https://www.algolia.com/doc/api-reference/api-parameters/enablePersonalization/)
361    enable_personalization: Option<bool>,
362    #[builder(setter(into))]
363    #[serde(skip_serializing_if = "Option::is_none")]
364    /// [https://www.algolia.com/doc/api-reference/api-parameters/userToken/](https://www.algolia.com/doc/api-reference/api-parameters/userToken/)
365    user_token: Option<String>,
366
367    // advanced
368    #[builder(setter(into))]
369    #[serde(skip_serializing_if = "Option::is_none")]
370    /// [https://www.algolia.com/doc/api-reference/api-parameters/distinct/](https://www.algolia.com/doc/api-reference/api-parameters/distinct/)
371    distinct: Option<crate::settings::Distinct>,
372    #[builder(setter(into))]
373    #[serde(skip_serializing_if = "Option::is_none")]
374    /// [https://www.algolia.com/doc/api-reference/api-parameters/getRankingInfo/](https://www.algolia.com/doc/api-reference/api-parameters/getRankingInfo/)
375    get_ranking_info: Option<bool>,
376    #[builder(setter(into))]
377    #[serde(skip_serializing_if = "Option::is_none")]
378    /// [https://www.algolia.com/doc/api-reference/api-parameters/clickAnalytics/](https://www.algolia.com/doc/api-reference/api-parameters/clickAnalytics/)
379    click_analytics: Option<bool>,
380    #[builder(setter(into))]
381    #[serde(skip_serializing_if = "Option::is_none")]
382    /// [https://www.algolia.com/doc/api-reference/api-parameters/analytics/](https://www.algolia.com/doc/api-reference/api-parameters/analytics/)
383    analytics: Option<bool>,
384    #[builder(setter(into))]
385    #[serde(skip_serializing_if = "Option::is_none")]
386    /// [https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/](https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/)
387    analytics_tags: Option<Vec<String>>,
388    #[builder(setter(into))]
389    #[serde(skip_serializing_if = "Option::is_none")]
390    /// [https://www.algolia.com/doc/api-reference/api-parameters/synonyms/](https://www.algolia.com/doc/api-reference/api-parameters/synonyms/)
391    synonyms: Option<bool>,
392    #[builder(setter(into))]
393    #[serde(skip_serializing_if = "Option::is_none")]
394    /// [https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInHighlight/](https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInHighlight/)
395    replace_synonyms_in_highlight: Option<bool>,
396    #[builder(setter(into))]
397    #[serde(skip_serializing_if = "Option::is_none")]
398    /// [https://www.algolia.com/doc/api-reference/api-parameters/minProximity/](https://www.algolia.com/doc/api-reference/api-parameters/minProximity/)
399    min_proximity: Option<crate::settings::MinProximity>,
400    #[builder(setter(into))]
401    #[serde(skip_serializing_if = "Option::is_none")]
402    /// [https://www.algolia.com/doc/api-reference/api-parameters/responseFields/](https://www.algolia.com/doc/api-reference/api-parameters/responseFields/)
403    response_fields: Option<Vec<String>>,
404    #[builder(setter(into))]
405    #[serde(skip_serializing_if = "Option::is_none")]
406    /// [https://www.algolia.com/doc/api-reference/api-parameters/maxFacetHits/](https://www.algolia.com/doc/api-reference/api-parameters/maxFacetHits/)
407    max_facet_hits: Option<u64>,
408    #[builder(setter(into))]
409    #[serde(skip_serializing_if = "Option::is_none")]
410    /// [https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/](https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/)
411    percentile_computation: Option<u64>,
412}
413
414#[cfg(test)]
415mod query_builder {
416    use super::*;
417    use serde_json;
418
419    #[test]
420    fn test_serialize() {
421        let query = SearchQueryBuilder::default()
422            .page(1)
423            .hits_per_page(6)
424            .build()
425            .unwrap();
426
427        assert_eq!(
428            serde_json::to_string(&query).unwrap(),
429            r#"{"query":null,"page":1,"hitsPerPage":6}"#
430        );
431    }
432}
433
434#[derive(Serialize)]
435struct SearchQueryBody {
436    params: String,
437}
438
439impl From<&str> for SearchQuery {
440    fn from(item: &str) -> Self {
441        SearchQuery {
442            query: Some(item.to_string()),
443            ..Default::default()
444        }
445    }
446}
447
448#[derive(Debug, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct AddObjectResult {
451    pub created_at: DateTime<Utc>,
452    #[serde(rename = "taskID")]
453    pub task_id: u64,
454    #[serde(rename = "objectID")]
455    pub object_id: String,
456}
457
458#[derive(Debug, Deserialize)]
459#[serde(rename_all = "camelCase")]
460pub struct UpdateOperationResult {
461    pub updated_at: DateTime<Utc>,
462    #[serde(rename = "taskID")]
463    pub task_id: u64,
464}
465
466#[derive(Debug, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct DeleteObjectResult {
469    pub deleted_at: DateTime<Utc>,
470    #[serde(rename = "taskID")]
471    pub task_id: u64,
472}
473
474#[derive(Serialize)]
475struct BatchedOperationItem<T> {
476    action: String,
477    body: T,
478}
479
480#[derive(Serialize)]
481struct BatchedOperation<T> {
482    requests: Vec<BatchedOperationItem<T>>,
483}
484
485#[derive(Debug, Deserialize)]
486pub struct BatchedOperatioResult {
487    #[serde(rename = "taskID")]
488    pub task_id: u64,
489    #[serde(rename = "objectIDs")]
490    pub object_ids: Vec<String>,
491}
492
493#[derive(Debug)]
494/// Index
495pub struct Index<T> {
496    /// Application id
497    pub application_id: String,
498    /// Index name
499    pub index_name: String,
500    pub(crate) api_key: String,
501    pub(crate) base_url: String,
502    pub(crate) index_type: PhantomData<T>,
503}
504
505impl<T: DeserializeOwned + Serialize> Index<T> {
506    /// Search the index.
507    /// This method accepts a [&str](https://doc.rust-lang.org/std/str/index.html):
508    /// ```no_run
509    /// # #[macro_use] extern crate serde_derive;
510    /// # use tokio;
511    /// # use algoliasearch::{Client, SearchQueryBuilder, Error};
512    /// # #[derive(Debug, Serialize, Deserialize)]
513    /// # struct User;
514    /// # #[tokio::main]
515    /// # async fn main() -> Result<(), Box<Error>> {
516    /// # let index = Client::default().init_index::<User>("users");
517    /// let res = index.search("Bernardo").await?;
518    /// dbg!(res.hits); // [User { name: "Bernardo", age: 32} ]
519    /// # Ok(())
520    /// # }
521    /// ```
522    /// Or a [SearchQuery](struct.SearchQuery.html) object, that can be build with the [SearchQueryBuilder](struct.SearchQueryBuilder.html):
523    /// ```no_run
524    /// # #[macro_use] extern crate serde_derive;
525    /// # use tokio;
526    /// # use algoliasearch::{settings::TypoTolerance, Client, SearchQueryBuilder, Error};
527    /// # #[derive(Serialize, Deserialize)]
528    /// # struct User;
529    /// # #[tokio::main]
530    /// # async fn main() -> Result<(), Box<Error>> {
531    /// #   let index = Client::default().init_index::<User>("users");
532    /// let query = SearchQueryBuilder::default()
533    ///     .query("Bernardo".to_string())
534    ///     .analytics(false)
535    ///     .typo_tolerance(TypoTolerance::Strict)
536    ///     .page(1)
537    ///     .build()
538    ///     .unwrap();
539    /// let res = index.search(query).await?;
540    /// #   Ok(())
541    /// # }
542    /// ```
543    pub async fn search(&self, query: impl Into<SearchQuery>) -> Result<SearchResult<T>, Error> {
544        let query = query.into();
545        let uri = format!("{}/indexes/{}/query", self.base_url, self.index_name);
546        let params = serde_urlencoded::to_string(query).expect("failed to encode params");
547        let params = &SearchQueryBody { params };
548        Client::new()
549            .post(&uri)
550            .headers(self.get_headers())
551            .json(&params)
552            .send()
553            .await?
554            .json::<SearchResult<T>>()
555            .await
556            .map_err(|e| e.into())
557    }
558    /// Get an object from the index.
559    /// ```no_run
560    /// # #[macro_use] extern crate serde_derive;
561    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
562    /// # #[derive(Serialize, Deserialize, Debug)]
563    /// # struct User;
564    /// # #[tokio::main]
565    /// # async fn main() -> Result<(), Box<Error>> {
566    /// #   let index = Client::default().init_index::<User>("users");
567    /// let object_1 = index.get_object(&"THE_ID", None).await?;
568    /// dbg!(object_1); // User { name: "Bernardo", age: 32 };
569    /// #   Ok(())
570    /// # }
571    /// ```
572    pub async fn get_object(
573        &self,
574        object_id: &str,
575        attributes_to_retrieve: Option<&[&str]>,
576    ) -> Result<T, Error> {
577        let uri = format!(
578            "{}/indexes/{}/{}",
579            self.base_url, self.index_name, object_id
580        );
581        Client::new()
582            .get(&uri)
583            .headers(self.get_headers())
584            .query(&[(
585                "attributes_to_retrieve",
586                attributes_to_retrieve.map(|el| el.join(",")),
587            )])
588            .send()
589            .await?
590            .json()
591            .await
592            .map_err(|e| e.into())
593    }
594    /// Add an object to the index.
595    /// ```no_run
596    /// # #[macro_use] extern crate serde_derive;
597    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
598    /// # #[derive(Serialize, Deserialize)]
599    /// # struct User { name: String, age: u32, }
600    /// # #[tokio::main]
601    /// # async fn main() -> Result<(), Box<Error>> {
602    /// #   let index = Client::default().init_index::<User>("users");
603    /// let object_1 = User { name: "Bernardo".into(), age: 32 };
604    /// index.add_object(&object_1).await?;
605    /// #   Ok(())
606    /// # }
607    /// ```
608    pub async fn add_object(&self, object: &T) -> Result<AddObjectResult, Error> {
609        let uri = format!("{}/indexes/{}", self.base_url, self.index_name);
610        Client::new()
611            .post(&uri)
612            .headers(self.get_headers())
613            .json(&object)
614            .send()
615            .await?
616            .json()
617            .await
618            .map_err(|e| e.into())
619    }
620    /// Add several objects to the index.
621    /// ```no_run
622    /// # #[macro_use] extern crate serde_derive;
623    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
624    /// # #[derive(Serialize, Deserialize)]
625    /// # struct User { name: String, age: u32, }
626    /// # #[tokio::main]
627    /// # async fn main() -> Result<(), Box<Error>> {
628    /// #   let index = Client::default().init_index::<User>("users");
629    /// let object_1 = User { name: "Bernardo".into(), age: 32 };
630    /// let object_2 = User { name: "Esmeralda".into(), age: 45 };
631    /// index.add_objects(&[&object_1, &object_2]).await?;
632    /// #   Ok(())
633    /// # }
634    /// ```
635    pub async fn add_objects(&self, objects: &[&T]) -> Result<BatchedOperatioResult, Error> {
636        let uri = format!("{}/indexes/{}/batch", self.base_url, self.index_name);
637        let requests = objects.iter().fold(vec![], |mut acc, x| {
638            acc.push(BatchedOperationItem {
639                action: "addObject".to_string(),
640                body: x,
641            });
642            acc
643        });
644        let requests = BatchedOperation { requests };
645        Client::new()
646            .post(&uri)
647            .headers(self.get_headers())
648            .json(&requests)
649            .send()
650            .await?
651            .json()
652            .await
653            .map_err(|e| e.into())
654    }
655    /// Add or replace an object with a given object ID.
656    /// If the object does not exist, it will be created. If it already exists, it will be replaced.
657    /// ```no_run
658    /// # #[macro_use] extern crate serde_derive;
659    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
660    /// # #[derive(Serialize, Deserialize)]
661    /// # struct User { object_id: String }
662    /// # #[tokio::main]
663    /// # async fn main() -> Result<(), Box<Error>> {
664    /// #   let index = Client::default().init_index::<User>("users");
665    /// #   let object_1 = User { object_id: String::from("123") };
666    /// index.update_object(&object_1, &object_1.object_id).await?;
667    /// #   Ok(())
668    /// # }
669    /// ```
670    pub async fn update_object(
671        &self,
672        object: &T,
673        object_id: &str,
674    ) -> Result<UpdateOperationResult, Error> {
675        let uri = format!(
676            "{}/indexes/{}/{}",
677            self.base_url, self.index_name, object_id
678        );
679        Client::new()
680            .put(&uri)
681            .headers(self.get_headers())
682            .json(object)
683            .send()
684            .await?
685            .json()
686            .await
687            .map_err(|e| e.into())
688    }
689    /// Add or replace several objects with a given object ID.
690    /// If the object does not exist, it will be created. If it already exists, it will be replaced..
691    /// ```no_run
692    /// # #[macro_use] extern crate serde_derive;
693    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
694    /// # #[derive(Serialize, Deserialize)]
695    /// # struct User;
696    /// # #[tokio::main]
697    /// # async fn main() -> Result<(), Box<Error>> {
698    /// #   let index = Client::default().init_index::<User>("users");
699    /// #   let object_1 = User;
700    /// #   let object_2 = User;
701    /// index.update_objects(&[&object_1, &object_2]).await?;
702    /// #   Ok(())
703    /// # }
704    /// ```
705    pub async fn update_objects(&self, objects: &[&T]) -> Result<BatchedOperatioResult, Error> {
706        let uri = format!("{}/indexes/{}/batch", self.base_url, self.index_name);
707        let requests = objects.iter().fold(vec![], |mut acc, x| {
708            acc.push(BatchedOperationItem {
709                action: "updateObject".to_string(),
710                body: x,
711            });
712            acc
713        });
714        let requests = BatchedOperation { requests };
715        Client::new()
716            .post(&uri)
717            .headers(self.get_headers())
718            .json(&requests)
719            .send()
720            .await?
721            .json()
722            .await
723            .map_err(|e| e.into())
724    }
725    /// Delete an object from the index.
726    /// ```no_run
727    /// # #[macro_use] extern crate serde_derive;
728    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
729    /// # #[derive(Serialize, Deserialize)]
730    /// # struct User { object_id: String, }
731    /// # #[tokio::main]
732    /// # async fn main() -> Result<(), Box<Error>> {
733    /// #   let index = Client::default().init_index::<User>("users");
734    /// #   let object_1 = User { object_id: "test".into(), };
735    /// index.delete_object(&object_1.object_id).await?;
736    /// #   Ok(())
737    /// # }
738    /// ```
739    pub async fn delete_object(&self, object_id: &str) -> Result<DeleteObjectResult, Error> {
740        let uri = format!(
741            "{}/indexes/{}/{}",
742            self.base_url, self.index_name, object_id
743        );
744        Client::new()
745            .delete(&uri)
746            .headers(self.get_headers())
747            .send()
748            .await?
749            .json()
750            .await
751            .map_err(|e| e.into())
752    }
753    /// Clear all objects from an index.
754    /// ```no_run
755    /// # #[macro_use] extern crate serde_derive;
756    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
757    /// # #[derive(Serialize, Deserialize)]
758    /// # struct User;
759    /// # #[tokio::main]
760    /// # async fn main() -> Result<(), Box<Error>> {
761    /// #   let index = Client::default().init_index::<User>("users");
762    /// index.clear_objects().await?;
763    /// #   Ok(())
764    /// # }
765    /// ```
766    pub async fn clear_objects(&self) -> Result<UpdateOperationResult, Error> {
767        let uri = format!("{}/indexes/{}/clear", self.base_url, self.index_name);
768        Client::new()
769            .post(&uri)
770            .headers(self.get_headers())
771            .send()
772            .await?
773            .json()
774            .await
775            .map_err(|e| e.into())
776    }
777    /// Get the index's settings.
778    /// ```no_run
779    /// # #[macro_use] extern crate serde_derive;
780    /// # use algoliasearch::{Error, Client, SearchQueryBuilder};
781    /// # #[derive(Serialize, Deserialize)]
782    /// # struct User;
783    /// # #[tokio::main]
784    /// # async fn main() -> Result<(), Box<Error>> {
785    /// #   let index = Client::default().init_index::<User>("users");
786    /// let settings = index.get_settings().await?;
787    /// dbg!(settings.hits_per_page); // 20
788    /// #   Ok(())
789    /// # }
790    /// ```
791    pub async fn get_settings(&self) -> Result<settings::IndexSettings, Error> {
792        let uri = format!("{}/indexes/{}/settings", self.base_url, self.index_name);
793        Client::new()
794            .get(&uri)
795            .headers(self.get_headers())
796            .send()
797            .await?
798            .json()
799            .await
800            .map_err(|e| e.into())
801    }
802    /// Set the index's settings.
803    /// ```no_run
804    /// # #[macro_use] extern crate serde_derive;
805    /// # use futures::Future;
806    /// # use algoliasearch::{
807    /// #    Client, SearchQueryBuilder,
808    /// #    Error,
809    /// #    settings::{IndexSettingsBuilder, SortFacetValuesBy}
810    /// # };
811    /// # #[derive(Serialize, Deserialize)]
812    /// # struct User;
813    /// # #[tokio::main]
814    /// # async fn main() -> Result<(), Box<Error>> {
815    /// #   let index = Client::default().init_index::<User>("users");
816    /// let settings = IndexSettingsBuilder::default()
817    ///     .hits_per_page(30)
818    ///     .sort_facet_values_by(SortFacetValuesBy::Count)
819    ///     .build()
820    ///     .unwrap();
821    /// index.set_settings(settings, None).await?;
822    /// #   Ok(())
823    /// # }
824    /// ```
825    pub async fn set_settings(
826        &self,
827        settings: settings::IndexSettings,
828        forward_to_replicas: Option<bool>,
829    ) -> Result<UpdateOperationResult, Error> {
830        let forward_to_replicas = forward_to_replicas.unwrap_or(false);
831        let uri = format!("{}/indexes/{}/settings", self.base_url, self.index_name);
832        Client::new()
833            .put(&uri)
834            .headers(self.get_headers())
835            .json(&settings)
836            .query(&[("forwardToReplicas", forward_to_replicas)])
837            .send()
838            .await?
839            .json()
840            .await
841            .map_err(|e| e.into())
842    }
843    // Build authentication headers.
844    fn get_headers(&self) -> HeaderMap {
845        let mut headers = HeaderMap::new();
846        headers.insert(
847            crate::APPLICATION_ID_HEADER,
848            self.application_id.parse().unwrap(),
849        );
850        headers.insert(crate::API_KEY_HEADER, self.api_key.parse().unwrap());
851        headers
852    }
853
854    /// Get a task's status.
855    /// ```no_run
856    /// # #[macro_use] extern crate serde_derive;
857    /// # use futures::Future;
858    /// # use algoliasearch::{
859    /// #    Client, SearchQueryBuilder,
860    /// #    Error,
861    /// #    settings::{IndexSettingsBuilder, SortFacetValuesBy}
862    /// # };
863    /// # #[derive(Serialize, Deserialize)]
864    /// # struct User;
865    /// # #[tokio::main]
866    /// # async fn main() -> Result<(), Box<Error>> {
867    /// #   let index = Client::default().init_index::<User>("users");
868    /// let task = index.get_task_status(123);
869    /// #   Ok(())
870    /// # }
871    /// ```
872    pub async fn get_task_status(&self, task_id: u64) -> Result<task::TaskStatus, Error> {
873        let uri = format!(
874            "{}/indexes/{}/task/{}",
875            self.base_url, self.index_name, task_id
876        );
877        Client::new()
878            .get(&uri)
879            .headers(self.get_headers())
880            .send()
881            .await?
882            .json()
883            .await
884            .map_err(|e| e.into())
885    }
886}