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}