rust_moysklad/
api_client.rs

1use std::fmt::{Debug, Display};
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use tracing::instrument;
6use uuid::Uuid;
7
8use crate::{
9    models::{
10        characteristic::{CharResponse, VariantCharacteristic},
11        CustomEntity, EntityResponse, Meta,
12    },
13    PriceType,
14};
15/// initialize api client
16///
17/// # Example
18///
19/// ```rust
20/// use anyhow::Result;
21/// use rust_moysklad::{Assortment, MoySkladApiClient};
22/// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
23/// #[tokio::main]
24/// async fn main() -> Result<()> {
25///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
26///     tracing_subscriber::registry()
27///         .with(
28///             tracing_subscriber::EnvFilter::try_from_default_env()
29///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
30///         )
31///         .with(tracing_subscriber::fmt::layer())
32///         .init();
33///     let assortment = client.get_all::<Assortment>().await?;
34///     if let Some(last) = assortment.last() {
35///         dbg!(last);
36///     }
37///     Ok(())
38/// }
39/// ```
40#[derive(Debug, Clone)]
41pub struct MoySkladApiClient {
42    token: String,
43}
44pub trait MsEntity: for<'a> Deserialize<'a> + Serialize + Clone + Debug {
45    fn url() -> String;
46}
47impl MoySkladApiClient {
48    /// initialize api client
49    ///
50    /// # Example
51    ///
52    /// ```rust
53    /// use anyhow::Result;
54    /// use rust_moysklad::MoySkladApiClient;
55    /// #[tokio::main]
56    /// async fn main() -> Result<()> {
57    ///     let client = MoySkladApiClient::from_env()?;
58    ///     //...do something...
59    ///     Ok(())
60    /// }
61    /// ```
62    pub fn from_env() -> Result<Self> {
63        let token = std::env::var("MS_TOKEN")?;
64        Ok(Self { token })
65    }
66    /// initialize api client
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// use anyhow::Result;
72    /// use rust_moysklad::MoySkladApiClient;
73    /// #[tokio::main]
74    /// async fn main() -> Result<()> {
75    ///     let token = std::env::var("MS_TOKEN")?;
76    ///     let client = MoySkladApiClient::new(token)?;
77    ///     //...do something...
78    ///     Ok(())
79    /// }
80    /// ```
81    pub fn new(token: impl AsRef<str>) -> Result<Self> {
82        let token = token.as_ref().to_owned();
83        Ok(Self { token })
84    }
85    /// retrieve list of entity
86    ///
87    /// # Example
88    ///
89    /// ```rust
90    /// use anyhow::Result;
91    /// use rust_moysklad::{Assortment, MoySkladApiClient};
92    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
93    /// #[tokio::main]
94    /// async fn main() -> Result<()> {
95    ///     tracing_subscriber::registry()
96    ///         .with(
97    ///             tracing_subscriber::EnvFilter::try_from_default_env()
98    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
99    ///         )
100    ///         .with(tracing_subscriber::fmt::layer())
101    ///         .init();
102    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
103    ///     let assortment = client.get_all::<Assortment>().await?;
104    ///     // ...do something...
105    ///     Ok(())
106    /// }
107    /// ```
108    #[instrument]
109    pub async fn get_all<E>(&self) -> Result<Vec<E>>
110    where
111        E: MsEntity,
112    {
113        static APP_USER_AGENT: &str =
114            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
115        let client = reqwest::Client::builder()
116            .user_agent(APP_USER_AGENT)
117            .gzip(true)
118            .build()?;
119        let limit = 500;
120        let mut offset = 0;
121        let mut result = Vec::new();
122        loop {
123            let uri = format!("{}?limit={limit}&offset={offset}", E::url());
124            let response = client.get(&uri).bearer_auth(&self.token).send().await?;
125            match response.status() {
126                reqwest::StatusCode::OK => {
127                    // let res: EntityResponse<E> = response.json().await?;
128                    let val: serde_json::Value = response.json().await?;
129                    if let Ok(res) = serde_json::from_value::<EntityResponse<E>>(val.clone()) {
130                        if res.rows.is_empty() {
131                            break;
132                        } else {
133                            result.extend(res.rows);
134                            offset += limit;
135                        }
136                    } else {
137                        let msg = format!("{val:#?}\n");
138                        return Err(anyhow::Error::msg(msg));
139                    }
140                }
141                _ => {
142                    let err_res: serde_json::Value = response.json().await?;
143                    let msg = format!("{err_res:#?}\n");
144                    return Err(anyhow::Error::msg(msg));
145                }
146            }
147        }
148        Ok(result)
149    }
150    /// get entity
151    ///
152    /// # Example
153    ///
154    /// ```rust
155    /// use anyhow::Result;
156    /// use rust_moysklad::{Currency, MoySkladApiClient};
157    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
158    /// #[tokio::main]
159    /// async fn main() -> Result<()> {
160    ///     tracing_subscriber::registry()
161    ///         .with(
162    ///             tracing_subscriber::EnvFilter::try_from_default_env()
163    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
164    ///         )
165    ///         .with(tracing_subscriber::fmt::layer())
166    ///         .init();
167    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
168    ///     let currencies = client.get_all::<Currency>().await?;
169    ///     if let Some(last) = currencies.last() {
170    ///         let last_currency = client.get::<Currency>(last.id).await?;
171    ///         dbg!(last_currency);
172    ///     }
173    ///     Ok(())
174    /// }
175    /// ```
176    #[instrument]
177    pub async fn get<E>(&self, id: Uuid) -> Result<E>
178    where
179        E: MsEntity,
180    {
181        static APP_USER_AGENT: &str =
182            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
183        let client = reqwest::Client::builder()
184            .user_agent(APP_USER_AGENT)
185            .gzip(true)
186            .build()?;
187        let uri = format!("{}/{id}", E::url());
188        let response = client.get(&uri).bearer_auth(&self.token).send().await?;
189        match response.status() {
190            reqwest::StatusCode::OK => {
191                let res: E = response.json().await?;
192                Ok(res)
193            }
194            _ => {
195                let err_res: serde_json::Value = response.json().await?;
196                let msg = format!("{err_res:#?}\n");
197                Err(anyhow::Error::msg(msg))
198            }
199        }
200    }
201    /// Create entity
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// use anyhow::Result;
207    /// use rust_moysklad::{FilterOperator, MoySkladApiClient, ProductFolder, TaxSystem};
208    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
209    /// #[tokio::main]
210    /// async fn main() -> Result<()> {
211    ///     tracing_subscriber::registry()
212    ///         .with(
213    ///             tracing_subscriber::EnvFilter::try_from_default_env()
214    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
215    ///         )
216    ///         .with(tracing_subscriber::fmt::layer())
217    ///         .init();
218    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
219    ///     let folders = client.get_all::<ProductFolder>().await?;
220    ///     if let Some(last) = folders.last() {
221    ///         let last_folder = client.get::<ProductFolder>(last.id).await?;
222    ///         dbg!(last_folder);
223    ///     }
224    ///     if let Some(ad) = folders.iter().find(|f| f.name == "Сопутствующие товары") {
225    ///         let folder_to_create = ProductFolder::create("Ковродержатели")
226    ///             .code("42")
227    ///             .description("Очень крутое описание")
228    ///             .external_code("69")
229    ///             .product_folder(ad.meta.clone())
230    ///             .shared(true)
231    ///             .tax_system(TaxSystem::SimplifiedTaxSystemIncomeOutcome)
232    ///             .use_parent_vat(true)
233    ///             .vat(0)
234    ///             .vat_enabled(false)
235    ///             .build();
236    ///         let created: ProductFolder = client.create(folder_to_create).await?;
237    ///         dbg!(&created);
238    ///         let update = ProductFolder::update().external_code("96").build();
239    ///         let updated: ProductFolder = client.update(created.id, update).await?;
240    ///         dbg!(&updated);
241    ///         let batch = vec![ProductFolder::update()
242    ///             .meta(created.meta)
243    ///             .tax_system(TaxSystem::TaxSystemSameAsGroup)
244    ///             .build()];
245    ///         let batch_updated: Vec<ProductFolder> = client.batch_create_update(batch).await?;
246    ///         dbg!(&batch_updated);
247    ///         client.delete::<ProductFolder>(updated.id).await?;
248    ///         let search_result = client.search::<ProductFolder>("сопут").await?;
249    ///         dbg!(&search_result);
250    ///         let filter_result = client
251    ///             .filter::<ProductFolder>("pathName", FilterOperator::PartialMatch, "Ковр")
252    ///             .await?;
253    ///         dbg!(filter_result.len());
254    ///     }
255    ///     Ok(())
256    /// }
257    /// ```
258    #[instrument]
259    pub async fn create<E, C>(&self, object: C) -> Result<E>
260    where
261        E: MsEntity,
262        C: Serialize + Debug,
263    {
264        static APP_USER_AGENT: &str =
265            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
266        let client = reqwest::Client::builder()
267            .user_agent(APP_USER_AGENT)
268            .gzip(true)
269            .build()?;
270        let response = client
271            .post(E::url())
272            .json(&object)
273            .bearer_auth(&self.token)
274            .send()
275            .await?;
276        match response.status() {
277            reqwest::StatusCode::OK => {
278                let res: E = response.json().await?;
279                Ok(res)
280            }
281            _ => {
282                let err_res: serde_json::Value = response.json().await?;
283                let msg = format!("{err_res:#?}\n");
284                Err(anyhow::Error::msg(msg))
285            }
286        }
287    }
288    /// Update entity
289    ///
290    /// # Example
291    ///
292    /// ```rust
293    /// use anyhow::Result;
294    /// use rust_moysklad::{FilterOperator, MoySkladApiClient, ProductFolder, TaxSystem};
295    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
296    /// #[tokio::main]
297    /// async fn main() -> Result<()> {
298    ///     tracing_subscriber::registry()
299    ///         .with(
300    ///             tracing_subscriber::EnvFilter::try_from_default_env()
301    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
302    ///         )
303    ///         .with(tracing_subscriber::fmt::layer())
304    ///         .init();
305    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
306    ///     let folders = client.get_all::<ProductFolder>().await?;
307    ///     if let Some(last) = folders.last() {
308    ///         let last_folder = client.get::<ProductFolder>(last.id).await?;
309    ///         dbg!(last_folder);
310    ///     }
311    ///     if let Some(ad) = folders.iter().find(|f| f.name == "Сопутствующие товары") {
312    ///         let folder_to_create = ProductFolder::create("Ковродержатели")
313    ///             .code("42")
314    ///             .description("Очень крутое описание")
315    ///             .external_code("69")
316    ///             .product_folder(ad.meta.clone())
317    ///             .shared(true)
318    ///             .tax_system(TaxSystem::SimplifiedTaxSystemIncomeOutcome)
319    ///             .use_parent_vat(true)
320    ///             .vat(0)
321    ///             .vat_enabled(false)
322    ///             .build();
323    ///         let created: ProductFolder = client.create(folder_to_create).await?;
324    ///         dbg!(&created);
325    ///         let update = ProductFolder::update().external_code("96").build();
326    ///         let updated: ProductFolder = client.update(created.id, update).await?;
327    ///         dbg!(&updated);
328    ///         let batch = vec![ProductFolder::update()
329    ///             .meta(created.meta)
330    ///             .tax_system(TaxSystem::TaxSystemSameAsGroup)
331    ///             .build()];
332    ///         let batch_updated: Vec<ProductFolder> = client.batch_create_update(batch).await?;
333    ///         dbg!(&batch_updated);
334    ///         client.delete::<ProductFolder>(updated.id).await?;
335    ///         let search_result = client.search::<ProductFolder>("сопут").await?;
336    ///         dbg!(&search_result);
337    ///         let filter_result = client
338    ///             .filter::<ProductFolder>("pathName", FilterOperator::PartialMatch, "Ковр")
339    ///             .await?;
340    ///         dbg!(filter_result.len());
341    ///     }
342    ///     Ok(())
343    /// }
344    /// ```
345    #[instrument]
346    pub async fn update<E, U>(&self, id: Uuid, object: U) -> Result<E>
347    where
348        E: MsEntity,
349        U: Debug + Serialize,
350    {
351        static APP_USER_AGENT: &str =
352            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
353        let client = reqwest::Client::builder()
354            .user_agent(APP_USER_AGENT)
355            .gzip(true)
356            .build()?;
357        let uri = format!("{}/{id}", E::url());
358        let response = client
359            .put(&uri)
360            .bearer_auth(&self.token)
361            .json(&object)
362            .send()
363            .await?;
364        match response.status() {
365            reqwest::StatusCode::OK => {
366                let res: E = response.json().await?;
367                Ok(res)
368            }
369            _ => {
370                let err_res: serde_json::Value = response.json().await?;
371                let msg = format!("{err_res:#?}\n");
372                Err(anyhow::Error::msg(msg))
373            }
374        }
375    }
376    /// Delete entity
377    ///
378    /// # Example
379    ///
380    /// ```rust
381    /// use anyhow::Result;
382    /// use rust_moysklad::{FilterOperator, MoySkladApiClient, ProductFolder, TaxSystem};
383    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
384    /// #[tokio::main]
385    /// async fn main() -> Result<()> {
386    ///     tracing_subscriber::registry()
387    ///         .with(
388    ///             tracing_subscriber::EnvFilter::try_from_default_env()
389    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
390    ///         )
391    ///         .with(tracing_subscriber::fmt::layer())
392    ///         .init();
393    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
394    ///     let folders = client.get_all::<ProductFolder>().await?;
395    ///     if let Some(last) = folders.last() {
396    ///         let last_folder = client.get::<ProductFolder>(last.id).await?;
397    ///         dbg!(last_folder);
398    ///     }
399    ///     if let Some(ad) = folders.iter().find(|f| f.name == "Сопутствующие товары") {
400    ///         let folder_to_create = ProductFolder::create("Ковродержатели")
401    ///             .code("42")
402    ///             .description("Очень крутое описание")
403    ///             .external_code("69")
404    ///             .product_folder(ad.meta.clone())
405    ///             .shared(true)
406    ///             .tax_system(TaxSystem::SimplifiedTaxSystemIncomeOutcome)
407    ///             .use_parent_vat(true)
408    ///             .vat(0)
409    ///             .vat_enabled(false)
410    ///             .build();
411    ///         let created: ProductFolder = client.create(folder_to_create).await?;
412    ///         dbg!(&created);
413    ///         let update = ProductFolder::update().external_code("96").build();
414    ///         let updated: ProductFolder = client.update(created.id, update).await?;
415    ///         dbg!(&updated);
416    ///         let batch = vec![ProductFolder::update()
417    ///             .meta(created.meta)
418    ///             .tax_system(TaxSystem::TaxSystemSameAsGroup)
419    ///             .build()];
420    ///         let batch_updated: Vec<ProductFolder> = client.batch_create_update(batch).await?;
421    ///         dbg!(&batch_updated);
422    ///         client.delete::<ProductFolder>(updated.id).await?;
423    ///         let search_result = client.search::<ProductFolder>("сопут").await?;
424    ///         dbg!(&search_result);
425    ///         let filter_result = client
426    ///             .filter::<ProductFolder>("pathName", FilterOperator::PartialMatch, "Ковр")
427    ///             .await?;
428    ///         dbg!(filter_result.len());
429    ///     }
430    ///     Ok(())
431    /// }
432    /// ```
433    #[instrument]
434    pub async fn delete<E>(&self, id: Uuid) -> Result<()>
435    where
436        E: MsEntity,
437    {
438        static APP_USER_AGENT: &str =
439            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
440        let client = reqwest::Client::builder()
441            .user_agent(APP_USER_AGENT)
442            .gzip(true)
443            .build()?;
444        let uri = format!("{}/{id}", E::url());
445        let response = client.delete(&uri).bearer_auth(&self.token).send().await?;
446        match response.status() {
447            reqwest::StatusCode::OK => Ok(()),
448            _ => {
449                let err_res: serde_json::Value = response.json().await?;
450                let msg = format!("{err_res:#?}\n");
451                Err(anyhow::Error::msg(msg))
452            }
453        }
454    }
455    /// Batch create/update entities
456    /// for updates required meta field
457    ///
458    /// # Example
459    ///
460    /// ```rust
461    /// use anyhow::Result;
462    /// use rust_moysklad::{FilterOperator, MoySkladApiClient, ProductFolder, TaxSystem};
463    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
464    /// #[tokio::main]
465    /// async fn main() -> Result<()> {
466    ///     tracing_subscriber::registry()
467    ///         .with(
468    ///             tracing_subscriber::EnvFilter::try_from_default_env()
469    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
470    ///         )
471    ///         .with(tracing_subscriber::fmt::layer())
472    ///         .init();
473    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
474    ///     let folders = client.get_all::<ProductFolder>().await?;
475    ///     if let Some(last) = folders.last() {
476    ///         let last_folder = client.get::<ProductFolder>(last.id).await?;
477    ///         dbg!(last_folder);
478    ///     }
479    ///     if let Some(ad) = folders.iter().find(|f| f.name == "Сопутствующие товары") {
480    ///         let folder_to_create = ProductFolder::create("Ковродержатели")
481    ///             .code("42")
482    ///             .description("Очень крутое описание")
483    ///             .external_code("69")
484    ///             .product_folder(ad.meta.clone())
485    ///             .shared(true)
486    ///             .tax_system(TaxSystem::SimplifiedTaxSystemIncomeOutcome)
487    ///             .use_parent_vat(true)
488    ///             .vat(0)
489    ///             .vat_enabled(false)
490    ///             .build();
491    ///         let created: ProductFolder = client.create(folder_to_create).await?;
492    ///         dbg!(&created);
493    ///         let update = ProductFolder::update().external_code("96").build();
494    ///         let updated: ProductFolder = client.update(created.id, update).await?;
495    ///         dbg!(&updated);
496    ///         let batch = vec![ProductFolder::update()
497    ///             .meta(created.meta)
498    ///             .tax_system(TaxSystem::TaxSystemSameAsGroup)
499    ///             .build()];
500    ///         let batch_updated: Vec<ProductFolder> = client.batch_create_update(batch).await?;
501    ///         dbg!(&batch_updated);
502    ///         client.delete::<ProductFolder>(updated.id).await?;
503    ///         let search_result = client.search::<ProductFolder>("сопут").await?;
504    ///         dbg!(&search_result);
505    ///         let filter_result = client
506    ///             .filter::<ProductFolder>("pathName", FilterOperator::PartialMatch, "Ковр")
507    ///             .await?;
508    ///         dbg!(filter_result.len());
509    ///     }
510    ///     Ok(())
511    /// }
512    /// ```
513    #[instrument]
514    pub async fn batch_create_update<E, C>(&self, objects: Vec<C>) -> Result<Vec<E>>
515    where
516        E: MsEntity,
517        C: Serialize + Debug,
518    {
519        static APP_USER_AGENT: &str =
520            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
521        let client = reqwest::Client::builder()
522            .user_agent(APP_USER_AGENT)
523            .gzip(true)
524            .build()?;
525        let response = client
526            .post(E::url())
527            .json(&objects)
528            .bearer_auth(&self.token)
529            .send()
530            .await?;
531        match response.status() {
532            reqwest::StatusCode::OK => {
533                let res: Vec<E> = response.json().await?;
534                Ok(res)
535            }
536            _ => {
537                let err_res: serde_json::Value = response.json().await?;
538                let msg = format!("{err_res:#?}\n");
539                Err(anyhow::Error::msg(msg))
540            }
541        }
542    }
543    pub async fn batch_delete<E>(&self, objects: Vec<impl Serialize>) -> Result<()>
544    where
545        E: MsEntity,
546    {
547        static APP_USER_AGENT: &str =
548            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
549        let client = reqwest::Client::builder()
550            .user_agent(APP_USER_AGENT)
551            .gzip(true)
552            .build()?;
553        let uri = format!("{}/delete", E::url());
554        let response = client
555            .post(&uri)
556            .json(&objects)
557            .bearer_auth(&self.token)
558            .send()
559            .await?;
560        match response.status() {
561            reqwest::StatusCode::OK => Ok(()),
562            _ => {
563                let err_res: serde_json::Value = response.json().await?;
564                let msg = format!("{err_res:#?}\n");
565                Err(anyhow::Error::msg(msg))
566            }
567        }
568    }
569    /// Контекстный поиск
570    /// В JSON API можно осуществлять контекстный поиск среди списка сущностей определенного типа по их строковым полям. Для этого используется URI параметр фильтрации search
571    ///
572    /// search Параметр фильтрации, с помощью которого можно осуществить поиск в списке сущностей. Поиск происходит по основным строковым полям сущностей данного типа. Результатом поиска будет отсортированный по релевантности список сущностей данного типа, прошедших фильтрацию по переданной поисковой строке. В отличии от фильтрации выборки с помощью параметра filter, при которой значения проверяются на точное совпадение указанным, при контекстном поиске проверка на совпадение не строгая. Таким образом, если осуществлять фильтрацию вида ../entity/?filter=name=120 в отфильтрованную выборку попадут только те сущности, поле name у которых имеет значение 120 и никакие другие. При контекстном поиске вида ../entity/?search=120 будут выведены как сущности с name равным 120, так и сущности, в имени (или в другом строковом поле) которых 120 является началом какого-то слова, например 12003, пазл детский 1200 штук и т.п. Причем, если ввести несколько слов ../entity/?search=120 возврат и поиск идет по полям name и description, то будут выведены как сущности с name равным 1200 и с description равным возврат из-за деффекта, так и сущности с именем 777 с описанием розничный возврат на улице 120 летия.
573    ///
574    /// Примеры запросов контекстного поиска (значения должны быть urlencoded):
575    /// https://api.moysklad.ru/api/remap/1.2/entity/project?search=реструктуризация
576    /// https://api.moysklad.ru/api/remap/1.2/entity/move?search=ул.Вавилова
577    /// https://api.moysklad.ru/api/remap/1.2/entity/counterparty?search=петров
578    /// use anyhow::Result;
579    /// use rust_moysklad::{FilterOperator, MoySkladApiClient, ProductFolder, TaxSystem};
580    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
581    /// #[tokio::main]
582    /// async fn main() -> Result<()> {
583    ///     tracing_subscriber::registry()
584    ///         .with(
585    ///             tracing_subscriber::EnvFilter::try_from_default_env()
586    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
587    ///         )
588    ///         .with(tracing_subscriber::fmt::layer())
589    ///         .init();
590    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
591    ///     let folders = client.get_all::<ProductFolder>().await?;
592    ///     if let Some(last) = folders.last() {
593    ///         let last_folder = client.get::<ProductFolder>(last.id).await?;
594    ///         dbg!(last_folder);
595    ///     }
596    ///     if let Some(ad) = folders.iter().find(|f| f.name == "Сопутствующие товары") {
597    ///         let folder_to_create = ProductFolder::create("Ковродержатели")
598    ///             .code("42")
599    ///             .description("Очень крутое описание")
600    ///             .external_code("69")
601    ///             .product_folder(ad.meta.clone())
602    ///             .shared(true)
603    ///             .tax_system(TaxSystem::SimplifiedTaxSystemIncomeOutcome)
604    ///             .use_parent_vat(true)
605    ///             .vat(0)
606    ///             .vat_enabled(false)
607    ///             .build();
608    ///         let created: ProductFolder = client.create(folder_to_create).await?;
609    ///         dbg!(&created);
610    ///         let update = ProductFolder::update().external_code("96").build();
611    ///         let updated: ProductFolder = client.update(created.id, update).await?;
612    ///         dbg!(&updated);
613    ///         let batch = vec![ProductFolder::update()
614    ///             .meta(created.meta)
615    ///             .tax_system(TaxSystem::TaxSystemSameAsGroup)
616    ///             .build()];
617    ///         let batch_updated: Vec<ProductFolder> = client.batch_create_update(batch).await?;
618    ///         dbg!(&batch_updated);
619    ///         client.delete::<ProductFolder>(updated.id).await?;
620    ///         let search_result = client.search::<ProductFolder>("сопут").await?;
621    ///         dbg!(&search_result);
622    ///         let filter_result = client
623    ///             .filter::<ProductFolder>("pathName", FilterOperator::PartialMatch, "Ковр")
624    ///             .await?;
625    ///         dbg!(filter_result.len());
626    ///     }
627    ///     Ok(())
628    /// }
629    pub async fn search<E>(&self, search_string: impl Into<String>) -> Result<Vec<E>>
630    where
631        E: MsEntity,
632    {
633        static APP_USER_AGENT: &str =
634            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
635        let client = reqwest::Client::builder()
636            .user_agent(APP_USER_AGENT)
637            .gzip(true)
638            .build()?;
639        let limit = 1000;
640        let mut offset = 0;
641        let mut result = Vec::new();
642        let search_string: String = search_string.into();
643        loop {
644            let response = client
645                .get(E::url())
646                .bearer_auth(&self.token)
647                .query(&[
648                    (limit.to_string(), offset.to_string()),
649                    ("search".to_string(), search_string.clone()),
650                ])
651                .send()
652                .await?;
653            match response.status() {
654                reqwest::StatusCode::OK => {
655                    let res: EntityResponse<E> = response.json().await?;
656                    if res.rows.is_empty() {
657                        break;
658                    } else {
659                        result.extend(res.rows);
660                        if let Some(size) = res.meta.size {
661                            if limit + offset > size {
662                                break;
663                            }
664                        }
665                        offset += limit;
666                    }
667                }
668                _ => {
669                    let err_res: serde_json::Value = response.json().await?;
670                    let msg = format!("{err_res:#?}\n");
671                    return Err(anyhow::Error::msg(msg));
672                }
673            }
674        }
675        Ok(result)
676    }
677    /// Фильтрация выборки с помощью параметра filter
678    /// Для фильтрации выборки по нескольким полям можно использовать url параметр filter. Значение этого параметра - urlencoded строка с поисковыми условиями, перечисленными через ;. Для использования самого символа ; в текстовых фильтрах необходимо указывать два символа \;. (Все примеры ниже указаны без urlencoded для лучшей читаемости) Каждое поисковое условие - это сочетание названия поля, оператора и константы. Фильтровать можно по всем полям, значения которых являются примитивными типами. Т.е. нельзя фильтровать поля-объекты и поля-массивы, все остальные поля могут быть использованы в параметре filter.
679    ///
680    /// Допустимые операторы: ['=', '>', '<', '>=', '<=', '!=', '~', '~=', '=~']
681    /// Если в поисковом запросе несколько раз встречается условие типа "равенство" = примененное к одному и тому же полю, то такое условие интерпретируется как совокупность условий, разделенных логическим оператором ИЛИ.
682    ///
683    /// Например условие filter=sum=100;sum=150 будет интерпретировано как sum=100 ИЛИ sum=150 или же sum in (100, 150)
684    /// Если же встречается несколько условий вида "не равно" !=, наложенных на одну и ту же переменную, то они интерпретируются как совокупность условий разделенных логическим оператором И.
685    ///
686    /// Например условие filter=name!=0001;name!=0002 будет эквивалентно следующим (взаимно эквивалентным) условиям : name != 0001 И name != 0002 или name not in (0001, 0002)
687    /// Если на одно из полей наложено ограничение типа "равенство", а затем на него накладывается ограничение типа неравенство - в таком случае произойдет ошибка.
688    ///
689    /// Например условие filter=sum=100;sum>99 вызовет ошибку.
690    /// Допускается использование одновременно нескольких одинаковых операторов сравнения ['>', '<', '>=', '<='] для одного поля. При этом будет использовано лишь первое значение.
691    ///
692    /// Например условие filter=sum>99;sum>100 будет аналогично условию filter=sum>99. В будущих версиях такое условие будет вызывать ошибку.
693    /// Фильтры, примененные к разным полям объединяются через логическое И, т.е. в запросе вида:
694    ///
695    /// filter=sum=100;moment>2016-10-11 12:00:00 выборка будет отфильтрована и по сумме и по дате.
696    ///
697    /// # Example
698    ///
699    /// ```rust
700    /// use anyhow::Result;
701    /// use rust_moysklad::{FilterOperator, MoySkladApiClient, ProductFolder, TaxSystem};
702    /// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
703    /// #[tokio::main]
704    /// async fn main() -> Result<()> {
705    ///     tracing_subscriber::registry()
706    ///         .with(
707    ///             tracing_subscriber::EnvFilter::try_from_default_env()
708    ///                 .unwrap_or_else(|_| "rust-moysklad=debug".into()),
709    ///         )
710    ///         .with(tracing_subscriber::fmt::layer())
711    ///         .init();
712    ///     let client = MoySkladApiClient::from_env().expect("MS_TOKEN env var not set!");
713    ///     let folders = client.get_all::<ProductFolder>().await?;
714    ///     if let Some(last) = folders.last() {
715    ///         let last_folder = client.get::<ProductFolder>(last.id).await?;
716    ///         dbg!(last_folder);
717    ///     }
718    ///     if let Some(ad) = folders.iter().find(|f| f.name == "Сопутствующие товары") {
719    ///         let folder_to_create = ProductFolder::create("Ковродержатели")
720    ///             .code("42")
721    ///             .description("Очень крутое описание")
722    ///             .external_code("69")
723    ///             .product_folder(ad.meta.clone())
724    ///             .shared(true)
725    ///             .tax_system(TaxSystem::SimplifiedTaxSystemIncomeOutcome)
726    ///             .use_parent_vat(true)
727    ///             .vat(0)
728    ///             .vat_enabled(false)
729    ///             .build();
730    ///         let created: ProductFolder = client.create(folder_to_create).await?;
731    ///         dbg!(&created);
732    ///         let update = ProductFolder::update().external_code("96").build();
733    ///         let updated: ProductFolder = client.update(created.id, update).await?;
734    ///         dbg!(&updated);
735    ///         let batch = vec![ProductFolder::update()
736    ///             .meta(created.meta)
737    ///             .tax_system(TaxSystem::TaxSystemSameAsGroup)
738    ///             .build()];
739    ///         let batch_updated: Vec<ProductFolder> = client.batch_create_update(batch).await?;
740    ///         dbg!(&batch_updated);
741    ///         client.delete::<ProductFolder>(updated.id).await?;
742    ///         let search_result = client.search::<ProductFolder>("сопут").await?;
743    ///         dbg!(&search_result);
744    ///         let filter_result = client
745    ///             .filter::<ProductFolder>("pathName", FilterOperator::PartialMatch, "Ковр")
746    ///             .await?;
747    ///         dbg!(filter_result.len());
748    ///     }
749    ///     Ok(())
750    /// }
751    /// ```
752    pub async fn filter<E>(
753        &self,
754        field: impl Into<String>,
755        operator: FilterOperator,
756        value: impl Into<String>,
757    ) -> Result<Vec<E>>
758    where
759        E: MsEntity,
760    {
761        static APP_USER_AGENT: &str =
762            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
763        let client = reqwest::Client::builder()
764            .user_agent(APP_USER_AGENT)
765            .gzip(true)
766            .build()?;
767        let limit = 1000;
768        let mut offset = 0;
769        let mut result = Vec::new();
770        let search_string = format!("{}{}{}", field.into(), operator, value.into());
771        loop {
772            let response = client
773                .get(E::url())
774                .bearer_auth(&self.token)
775                .query(&[
776                    (limit.to_string(), offset.to_string()),
777                    ("filter".to_string(), search_string.clone()),
778                ])
779                .send()
780                .await?;
781            match response.status() {
782                reqwest::StatusCode::OK => {
783                    let res: EntityResponse<E> = response.json().await?;
784                    if res.rows.is_empty() {
785                        break;
786                    } else {
787                        result.extend(res.rows);
788                        if let Some(size) = res.meta.size {
789                            if limit + offset > size {
790                                break;
791                            }
792                        }
793                        offset += limit;
794                    }
795                }
796                _ => {
797                    let err_res: serde_json::Value = response.json().await?;
798                    let msg = format!("{err_res:#?}\n");
799                    return Err(anyhow::Error::msg(msg));
800                }
801            }
802        }
803        Ok(result)
804    }
805    /// Типы цен
806    pub async fn get_price_types(&self) -> Result<Vec<PriceType>> {
807        let uri = "https://api.moysklad.ru/api/remap/1.2/context/companysettings/pricetype";
808        static APP_USER_AGENT: &str =
809            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
810        let client = reqwest::Client::builder()
811            .user_agent(APP_USER_AGENT)
812            .gzip(true)
813            .build()?;
814        let result: Vec<PriceType> = client
815            .get(uri)
816            .bearer_auth(&self.token)
817            .send()
818            .await?
819            .json()
820            .await?;
821        Ok(result)
822    }
823    /// Получить элементы справочника
824    pub async fn get_custom_entities(&self, customentity_meta: &Meta) -> Result<Vec<CustomEntity>> {
825        let path = customentity_meta.href.clone();
826        let id_vec = path.split('/').collect::<Vec<&str>>();
827        let id = id_vec
828            .last()
829            .ok_or(anyhow::Error::msg("error getting dictionary id"))?;
830        let uri = format!("https://api.moysklad.ru/api/remap/1.2/entity/customentity/{id}");
831        static APP_USER_AGENT: &str =
832            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
833        let client = reqwest::Client::builder()
834            .user_agent(APP_USER_AGENT)
835            .gzip(true)
836            .build()?;
837        let response = client.get(uri).bearer_auth(&self.token).send().await?;
838        match response.status() {
839            reqwest::StatusCode::OK => {
840                let res: EntityResponse<CustomEntity> = response.json().await?;
841                Ok(res.rows)
842            }
843            _ => {
844                let err_res: serde_json::Value = response.json().await?;
845                let msg = format!("{err_res:#?}\n");
846                Err(anyhow::Error::msg(msg))
847            }
848        }
849    }
850    /// Характеристики модификаций
851    pub async fn get_variants_characteristics(&self) -> Result<Vec<VariantCharacteristic>> {
852        let uri = "https://api.moysklad.ru/api/remap/1.2/entity/variant/metadata";
853        static APP_USER_AGENT: &str =
854            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
855        let client = reqwest::Client::builder()
856            .user_agent(APP_USER_AGENT)
857            .gzip(true)
858            .build()?;
859        let response = client.get(uri).bearer_auth(&self.token).send().await?;
860        match response.status() {
861            reqwest::StatusCode::OK => {
862                let res: CharResponse = response.json().await?;
863                Ok(res.characteristics)
864            }
865            _ => {
866                let err_res: serde_json::Value = response.json().await?;
867                let msg = format!("{err_res:#?}\n");
868                Err(anyhow::Error::msg(msg))
869            }
870        }
871    }
872}
873/// Доступные операторы для фильтрации
874pub enum FilterOperator {
875    /// `=` - фильтрация по значению
876    Equal,
877    /// `~` - частичное совпадение
878    PartialMatch,
879    /// `!~` - частичное совпадение не выводится
880    NoPartialMatch,
881    /// `~=` - полное совпадение в начале значения
882    FullMatchAtTheBeginning,
883    /// `=~` - полное совпадение в конце значения
884    CompleteMatchAtTheEnd,
885    /// `>` - больше
886    GreaterThan,
887    /// `<` - меньше
888    LesserThan,
889    /// `>=` - больше или равно
890    GreaterThanOrEqual,
891    /// `<=` - меньше или равно
892    LesserThanOrEqual,
893}
894impl Display for FilterOperator {
895    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
896        match self {
897            FilterOperator::Equal => write!(f, "="),
898            FilterOperator::PartialMatch => write!(f, "~"),
899            FilterOperator::NoPartialMatch => write!(f, "!~"),
900            FilterOperator::FullMatchAtTheBeginning => write!(f, "~="),
901            FilterOperator::CompleteMatchAtTheEnd => write!(f, "=~"),
902            FilterOperator::GreaterThan => write!(f, ">"),
903            FilterOperator::LesserThan => write!(f, "<"),
904            FilterOperator::GreaterThanOrEqual => write!(f, ">="),
905            FilterOperator::LesserThanOrEqual => write!(f, "<="),
906        }
907    }
908}