shipbob/lib.rs
1//! A fully generated, opinionated API client library for ShipBob.
2//!
3//! [](https://docs.rs/shipbob)
4//!
5//! ## API Details
6//!
7//! ShipBob Developer API Documentation
8//!
9//! # Authentication
10//!
11//! <!-- ReDoc-Inject: <security-definitions> -->
12//!
13//!
14//!
15//!
16//!
17//!
18//! ## Client Details
19//!
20//! This client is generated from the [ShipBob OpenAPI
21//! specs](https://developer.shipbob.com/c196c993-6cf8-4901-84aa-b425f3448df3) based on API spec version `1.0`. This way it will remain
22//! up to date as features are added. The documentation for the crate is generated
23//! along with the code to make this library easy to use.
24//!
25//!
26//! To install the library, add the following to your `Cargo.toml` file.
27//!
28//! ```toml
29//! [dependencies]
30//! shipbob = "0.7.0"
31//! ```
32//!
33//! ## Basic example
34//!
35//! Typical use will require intializing a `Client`. This requires
36//! a user agent string and set of credentials.
37//!
38//! ```rust
39//! use shipbob::Client;
40//!
41//! let shipbob = Client::new(
42//! String::from("api-key"),
43//! );
44//! ```
45//!
46//! Alternatively, the library can search for most of the variables required for
47//! the client in the environment:
48//!
49//! - `SHIPBOB_API_KEY`
50//!
51//! And then you can create a client from the environment.
52//!
53//! ```rust
54//! use shipbob::Client;
55//!
56//! let shipbob = Client::new_from_env();
57//! ```
58//!
59#![allow(clippy::derive_partial_eq_without_eq)]
60#![allow(clippy::too_many_arguments)]
61#![allow(clippy::nonstandard_macro_braces)]
62#![allow(clippy::large_enum_variant)]
63#![allow(clippy::tabs_in_doc_comments)]
64#![allow(missing_docs)]
65#![cfg_attr(docsrs, feature(doc_cfg))]
66
67/// Use the Channel Resource to list “channels” which you have access to. You will use this channelId for subsequent API calls made to ShipBob endpoints.
68///
69/// A channel is a specific installation of an application built by a vendor on top of our API – e.g. Kevin’s Shopify Store #133432. All write and most read endpoints require a channel to be passed in the header to complete the request. The channel is used to Identify where the data originally came from.
70///
71/// Applications that are granted multi-channel permissions will be able to read data from all channels that belong to a user. However, multi-channel applications will only be able to write on behalf of their own channel.
72pub mod channels;
73/// Use the Inventory Resource to retrieve ShipBob inventory items and quantities.
74///
75/// An inventory item is a representation of a physical good, that may or may not have physical stock in ShipBob’s fulfillment centers. Every product will have one or more inventory items mapped to it. A bundle product (a set of products that are sold as one combined package - e.g. gift or multi packs) is an example of a product that has 2 or more inventory items mapped to it.
76///
77/// Lot items are physical items that have expiration dates or batch numbers that should be fulfilled in a FIFO (first in, first out) manner. Most food items are lot items. Quantities by lot # and/or expiration date are also listed in the Inventory object.
78pub mod inventory;
79/// Use this API to interact with the physical locations across ShipBob's fulfillment network.
80///
81/// An active ShipBob location is operational for fulfillment processes, including receiving inventory and processing returns. It's important to note that some locations, access is granted to all merchants by default, while some locations require special request for merchants to be granted access.
82///
83/// For each location, determine if it can be leveraged for the user by viewing the access_granted & receiving_enabled fields.
84pub mod locations;
85/// > Note: The orderId in the API response will not match the Id displayed in the ShipBob Merchant Portal when you navigate to the Orders page. ShipBob is currently undergoing a schema migration and the Id displayed in the ShipBob Merchant Portal is the shipmentId not the orderId. In the future, the portal will display both orderId(s) and shipmentId(s).
86///
87/// Use the Orders Resource to create and retrieve orders in ShipBob.
88///
89/// An order a digital record of a complete purchase that comes from an upstream source (i.e. Shopify) and is intended for ShipBob to fulfill. The order object includes products purchased, shipping address details, shipping method selected etc. Orders are created in ShipBob via a channel.
90///
91/// When ShipBob fulfills the order, one or more shipments are created for that order. A shipment is a record of the physical package(s) sent out via a carrier. If an order is shipped in multiple packages then 2 or more shipments can be created for that order.
92///
93/// ### Tips for creating orders in ShipBob via the POST Order endpoint:
94///
95/// * Populate the referenceId with a unique and immutable order identifier from your upstream system. This field was created to allow you to tieback records in ShipBob with your upstream system.
96///
97/// * Ensure that the <em> shipping method </em> passed in the API request matches exactly what the user has listed as the <em> shipping method </em> on the <em> Ship Option Mapping </em> setup page in the ShipBob Merchant Portal. If they don’t match, ShipBob will assume that the user wants to leverage ShipBob’s default shipping method.
98///
99/// * You can leverage either productId (the ShipBob productId) or the product referenceId (your system's unique Identifier for products) when creating an order.
100///
101/// Use the Shipments endpoints to retrieve fulfillment information for shipments or orders.
102///
103/// A shipment is an object that is the result of a fulfillment of an order. An order can have one or more shipments. Say Shopify order #122323 contains 3 different products, shipped in two separate packages, there would be 2 shipments for that order.
104///
105/// Serial numbers are unique identifiers for an individual item (e.g. your specific iPhone X that you bought at the Apple Store). No inventory item can possess duplicate serial numbers. Merchants can request “serial scan”, which means ShipBob will capture the serial number(s) upon sending a shipment so the merchant knows which customer received which individual item(s).
106pub mod orders;
107/// Use the Products Resource to retrieve and create product records in ShipBob.
108///
109/// A product is a virtual record created in ShipBob’s system via a channel. Say a merchant has two Shopify stores (each store would have its own channel), Kevin’s Shopify Store #133 and Kevin’s Shopify store #134. If the same SKU was sold on both stores, two products would be created for that SKU, one product would be created to represent the SKU sold on Store #133 and one to represent it on Store on #134, with productIds 3884009 and 3884008 respectively.
110///
111/// While a product is a virtual record, the inventory item is a representation of a physical good. So in the above example, as product 3884009 and product 3884008 represent the same SKU sold on different channels, the same inventory item will be mapped to both products. Every product will have one or more inventory items mapped to it. Bundle products, a set of products that are sold to consumers as one combined package, think gift or multi packs, may have 2 or more inventory items mapped to them.
112///
113/// ### Tips for creating products in ShipBob via the POST Product endpoints:
114///
115/// * ShipBob needs products to be created at the lowest level. So if a product has 3 variants, small, medium and large, a separate product needs to be created in ShipBob for all three.
116///
117/// * Populate the referenceId with a unique and immutable product identifier from your upstream system. This field was created to allow you to tie back records in ShipBob with your upstream system.
118///
119/// * Use specific and/or unique names to describe each product so they can be easily identified by users in the ShipBob Merchant Portal. In particular, when creating variants, please give them distinguishable names i.e. for a Blue shirt that comes in two sizes, small and medium, strong product names would be Blue shirt size:small and Blue shirt size:medium.
120///
121/// > **NOTE:** The productId returned in the API response will not match the id displayed in the ShipBob Merchant Portal when you navigate to Inventory > Products. ShipBob is currently undergoing a schema migration and the Id displayed in the ShipBob Merchant Portal is the inventoryId not the productId. In the future, the portal will display both productId(s) and inventoryId(s).
122pub mod products;
123/// Use the Receiving Resource to retrieve, create and cancel Warehouse Receiving Orders (WROs).
124///
125/// A WRO is a request form that tells ShipBob's fulfillment centers what inventory should be received and stocked. Some other solutions call this an “ASN” or Advanced Ship Notice. WROs may include multiple inventory items with specific quantities. More details on creating a WRO can be found [here](https://support.shipbob.com/s/article/New-Send-Inventory-to-ShipBob-WRO).
126///
127/// A WRO can only be **canceled** if it is in the Awaiting status. WROs in Awaiting status are considered to still be in transit to ShipBob FCs. WROs that have Partially Arrived, have been Processed or are Completed, cannot be canceled.
128pub mod receiving;
129/// **While the Returns API is live, ShipBob's end to end Returns process will not go live until the beginning of March. As a result, any returns arriving at ShipBob's fulfillment centers prior to March 12st, 2020 will NOT be processed**.
130///
131/// Use the Returns resource to retrieve, create, edit and cancel return records in ShipBob.
132///
133/// A return is a request for ShipBob to perform an action on inventory that is coming back into our fulfillment centers. Typically, the return is a result of an order being requested to be refunded or exchanged. ShipBob does not handle refunds or exchanges - we simply process the inventory according to the merchant specifications.
134///
135/// Returns can only be **modified** or **cancelled** when they are in the Awaiting Arrival status. Returns that are being Processed or have been Completed cannot be modified or cancelled.
136///
137/// ### Tips for creating return orders:
138///
139/// * Populate the referenceId with a unique and immutable return identifier from your upstream system. This field was created to allow you to tie back records in ShipBob with your upstream system.
140///
141/// *Include each inventoryId exactly once in the inventory object. If an inventoryId is included more than once, the call will return an error code
142///
143/// * Provide a tracking # when submitting a return, while it is not a required field, it is the the most surefire way for ShipBob staff to properly and quickly identify the return package when it reaches our fulfillment center.
144///
145/// * Only include inventory items to the return record that will be returned in the same box i.e. if InventoryId 12232 and InventoryId 12039 will be returned in two seperate boxes, two return orders should be created.
146///
147/// * ShipBob does not process returns for digital items or bundle inventory items. Return calls that include digital inventory items (e.g. ebooks) or bundle inventory items (i.e. multipacks, combination of multiple inventory items) will return an error code.
148///
149/// * If you choose to provide a requested action (it is an optional field), only provide one requested action per inventory item. So if you have more than 1 quantity of a given item being returned within the same box, all quantities of the item have to have the same action associated with them. If you don’t provide a requested action, it will default to the action the User set for that inventory item in the ShipBob Merchant portal.
150pub mod returns;
151pub mod types;
152#[doc(hidden)]
153pub mod utils;
154/// Use the Webhooks Resource to create, view or delete subscriptions for a user.
155pub mod webhooks;
156
157pub use reqwest::{header::HeaderMap, StatusCode};
158
159#[derive(Debug)]
160pub struct Response<T> {
161 pub status: reqwest::StatusCode,
162 pub headers: reqwest::header::HeaderMap,
163 pub body: T,
164}
165
166impl<T> Response<T> {
167 pub fn new(status: reqwest::StatusCode, headers: reqwest::header::HeaderMap, body: T) -> Self {
168 Self {
169 status,
170 headers,
171 body,
172 }
173 }
174}
175
176type ClientResult<T> = Result<T, ClientError>;
177
178use thiserror::Error;
179
180/// Errors returned by the client
181#[derive(Debug, Error)]
182pub enum ClientError {
183 /// utf8 convertion error
184 #[error(transparent)]
185 FromUtf8Error(#[from] std::string::FromUtf8Error),
186 /// URL Parsing Error
187 #[error(transparent)]
188 UrlParserError(#[from] url::ParseError),
189 /// Serde JSON parsing error
190 #[error(transparent)]
191 SerdeJsonError(#[from] serde_json::Error),
192 /// Errors returned by reqwest
193 #[error(transparent)]
194 ReqwestError(#[from] reqwest::Error),
195 /// Errors returned by reqwest::header
196 #[error(transparent)]
197 InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
198 /// Errors returned by reqwest middleware
199 #[error(transparent)]
200 ReqwestMiddleWareError(#[from] reqwest_middleware::Error),
201 /// Generic HTTP Error
202 #[error("HTTP Error. Code: {status}, message: {error}")]
203 HttpError {
204 status: http::StatusCode,
205 headers: reqwest::header::HeaderMap,
206 error: String,
207 },
208}
209
210pub const FALLBACK_HOST: &str = "https://api.shipbob.com/1.0";
211
212mod progenitor_support {
213 use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
214
215 const PATH_SET: &AsciiSet = &CONTROLS
216 .add(b' ')
217 .add(b'"')
218 .add(b'#')
219 .add(b'<')
220 .add(b'>')
221 .add(b'?')
222 .add(b'`')
223 .add(b'{')
224 .add(b'}');
225
226 #[allow(dead_code)]
227 pub(crate) fn encode_path(pc: &str) -> String {
228 utf8_percent_encode(pc, PATH_SET).to_string()
229 }
230}
231
232#[derive(Debug, Default)]
233pub(crate) struct Message {
234 pub body: Option<reqwest::Body>,
235 pub content_type: Option<String>,
236}
237
238use std::env;
239
240#[derive(Debug, Default, Clone)]
241pub struct RootDefaultServer {}
242
243impl RootDefaultServer {
244 pub fn default_url(&self) -> &str {
245 "https://api.shipbob.com/1.0/"
246 }
247}
248
249/// Entrypoint for interacting with the API client.
250#[derive(Clone)]
251pub struct Client {
252 host: String,
253 host_override: Option<String>,
254 token: String,
255
256 client: reqwest_middleware::ClientWithMiddleware,
257}
258
259impl Client {
260 /// Create a new Client struct.
261 ///
262 /// # Panics
263 ///
264 /// This function will panic if the internal http client fails to create
265 pub fn new<T>(token: T) -> Self
266 where
267 T: ToString,
268 {
269 let client = reqwest::Client::builder()
270 .redirect(reqwest::redirect::Policy::none())
271 .build();
272 let retry_policy =
273 reqwest_retry::policies::ExponentialBackoff::builder().build_with_max_retries(3);
274 match client {
275 Ok(c) => {
276 let client = reqwest_middleware::ClientBuilder::new(c)
277 // Trace HTTP requests. See the tracing crate to make use of these traces.
278 .with(reqwest_tracing::TracingMiddleware::default())
279 // Retry failed requests.
280 .with(reqwest_conditional_middleware::ConditionalMiddleware::new(
281 reqwest_retry::RetryTransientMiddleware::new_with_policy(retry_policy),
282 |req: &reqwest::Request| req.try_clone().is_some(),
283 ))
284 .build();
285
286 let host = RootDefaultServer::default().default_url().to_string();
287
288 Client {
289 host,
290 host_override: None,
291 token: token.to_string(),
292
293 client,
294 }
295 }
296 Err(e) => panic!("creating reqwest client failed: {:?}", e),
297 }
298 }
299
300 /// Override the host for all endpoins in the client.
301 pub fn with_host_override<H>(&mut self, host: H) -> &mut Self
302 where
303 H: ToString,
304 {
305 self.host_override = Some(host.to_string());
306 self
307 }
308
309 /// Disables the global host override for the client.
310 pub fn remove_host_override(&mut self) -> &mut Self {
311 self.host_override = None;
312 self
313 }
314
315 pub fn get_host_override(&self) -> Option<&str> {
316 self.host_override.as_deref()
317 }
318
319 pub(crate) fn url(&self, path: &str, host: Option<&str>) -> String {
320 format!(
321 "{}{}",
322 self.get_host_override()
323 .or(host)
324 .unwrap_or(self.host.as_str()),
325 path
326 )
327 }
328
329 /// Create a new Client struct from environment variables.
330 ///
331 /// The following environment variables are expected to be set:
332 /// * `SHIPBOB_API_KEY`
333 ///
334 /// # Panics
335 ///
336 /// This function will panic if the expected environment variables can not be found
337 pub fn new_from_env() -> Self {
338 let token = env::var("SHIPBOB_API_KEY").expect("must set SHIPBOB_API_KEY");
339
340 Client::new(token)
341 }
342
343 async fn url_and_auth(&self, uri: &str) -> ClientResult<(reqwest::Url, Option<String>)> {
344 let parsed_url = uri.parse::<reqwest::Url>()?;
345 let auth = format!("Bearer {}", self.token);
346 Ok((parsed_url, Some(auth)))
347 }
348
349 async fn request_raw(
350 &self,
351 method: reqwest::Method,
352 uri: &str,
353 message: Message,
354 ) -> ClientResult<reqwest::Response> {
355 let (url, auth) = self.url_and_auth(uri).await?;
356 let instance = <&Client>::clone(&self);
357 let mut req = instance.client.request(method.clone(), url);
358 // Set the default headers.
359 req = req.header(
360 reqwest::header::ACCEPT,
361 reqwest::header::HeaderValue::from_static("application/json"),
362 );
363
364 if let Some(content_type) = &message.content_type {
365 req = req.header(
366 reqwest::header::CONTENT_TYPE,
367 reqwest::header::HeaderValue::from_str(content_type).unwrap(),
368 );
369 } else {
370 req = req.header(
371 reqwest::header::CONTENT_TYPE,
372 reqwest::header::HeaderValue::from_static("application/json"),
373 );
374 }
375
376 if let Some(auth_str) = auth {
377 req = req.header(http::header::AUTHORIZATION, &*auth_str);
378 }
379 if let Some(body) = message.body {
380 req = req.body(body);
381 }
382 Ok(req.send().await?)
383 }
384
385 async fn request<Out>(
386 &self,
387 method: reqwest::Method,
388 uri: &str,
389 message: Message,
390 ) -> ClientResult<crate::Response<Out>>
391 where
392 Out: serde::de::DeserializeOwned + 'static + Send,
393 {
394 let response = self.request_raw(method, uri, message).await?;
395
396 let status = response.status();
397 let headers = response.headers().clone();
398
399 let response_body = response.bytes().await?;
400
401 if status.is_success() {
402 log::debug!("Received successful response. Read payload.");
403 let parsed_response = if status == http::StatusCode::NO_CONTENT
404 || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
405 {
406 serde_json::from_str("null")?
407 } else {
408 serde_json::from_slice::<Out>(&response_body)?
409 };
410 Ok(crate::Response::new(status, headers, parsed_response))
411 } else {
412 let error = if response_body.is_empty() {
413 ClientError::HttpError {
414 status,
415 headers,
416 error: "empty response".into(),
417 }
418 } else {
419 ClientError::HttpError {
420 status,
421 headers,
422 error: String::from_utf8_lossy(&response_body).into(),
423 }
424 };
425
426 Err(error)
427 }
428 }
429
430 async fn request_with_links<Out>(
431 &self,
432 method: http::Method,
433 uri: &str,
434 message: Message,
435 ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Out>)>
436 where
437 Out: serde::de::DeserializeOwned + 'static + Send,
438 {
439 let response = self.request_raw(method, uri, message).await?;
440
441 let status = response.status();
442 let headers = response.headers().clone();
443 let link = response
444 .headers()
445 .get(http::header::LINK)
446 .and_then(|l| l.to_str().ok())
447 .and_then(|l| parse_link_header::parse(l).ok())
448 .as_ref()
449 .and_then(crate::utils::next_link);
450
451 let response_body = response.bytes().await?;
452
453 if status.is_success() {
454 log::debug!("Received successful response. Read payload.");
455
456 let parsed_response = if status == http::StatusCode::NO_CONTENT
457 || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
458 {
459 serde_json::from_str("null")?
460 } else {
461 serde_json::from_slice::<Out>(&response_body)?
462 };
463 Ok((link, crate::Response::new(status, headers, parsed_response)))
464 } else {
465 let error = if response_body.is_empty() {
466 ClientError::HttpError {
467 status,
468 headers,
469 error: "empty response".into(),
470 }
471 } else {
472 ClientError::HttpError {
473 status,
474 headers,
475 error: String::from_utf8_lossy(&response_body).into(),
476 }
477 };
478 Err(error)
479 }
480 }
481
482 /* TODO: make this more DRY */
483 #[allow(dead_code)]
484 async fn post_form<Out>(
485 &self,
486 uri: &str,
487 form: reqwest::multipart::Form,
488 ) -> ClientResult<crate::Response<Out>>
489 where
490 Out: serde::de::DeserializeOwned + 'static + Send,
491 {
492 let (url, auth) = self.url_and_auth(uri).await?;
493
494 let instance = <&Client>::clone(&self);
495
496 let mut req = instance.client.request(http::Method::POST, url);
497
498 // Set the default headers.
499 req = req.header(
500 reqwest::header::ACCEPT,
501 reqwest::header::HeaderValue::from_static("application/json"),
502 );
503
504 if let Some(auth_str) = auth {
505 req = req.header(http::header::AUTHORIZATION, &*auth_str);
506 }
507
508 req = req.multipart(form);
509
510 let response = req.send().await?;
511
512 let status = response.status();
513 let headers = response.headers().clone();
514
515 let response_body = response.bytes().await?;
516
517 if status.is_success() {
518 log::debug!("Received successful response. Read payload.");
519 let parsed_response = if status == http::StatusCode::NO_CONTENT
520 || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
521 {
522 serde_json::from_str("null")?
523 } else if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<String>() {
524 // Parse the output as a string.
525 let s = String::from_utf8(response_body.to_vec())?;
526 serde_json::from_value(serde_json::json!(&s))?
527 } else {
528 serde_json::from_slice::<Out>(&response_body)?
529 };
530 Ok(crate::Response::new(status, headers, parsed_response))
531 } else {
532 let error = if response_body.is_empty() {
533 ClientError::HttpError {
534 status,
535 headers,
536 error: "empty response".into(),
537 }
538 } else {
539 ClientError::HttpError {
540 status,
541 headers,
542 error: String::from_utf8_lossy(&response_body).into(),
543 }
544 };
545
546 Err(error)
547 }
548 }
549
550 /* TODO: make this more DRY */
551 #[allow(dead_code)]
552 async fn request_with_accept_mime<Out>(
553 &self,
554 method: reqwest::Method,
555 uri: &str,
556 accept_mime_type: &str,
557 ) -> ClientResult<crate::Response<Out>>
558 where
559 Out: serde::de::DeserializeOwned + 'static + Send,
560 {
561 let (url, auth) = self.url_and_auth(uri).await?;
562
563 let instance = <&Client>::clone(&self);
564
565 let mut req = instance.client.request(method, url);
566
567 // Set the default headers.
568 req = req.header(
569 reqwest::header::ACCEPT,
570 reqwest::header::HeaderValue::from_str(accept_mime_type)?,
571 );
572
573 if let Some(auth_str) = auth {
574 req = req.header(http::header::AUTHORIZATION, &*auth_str);
575 }
576
577 let response = req.send().await?;
578
579 let status = response.status();
580 let headers = response.headers().clone();
581
582 let response_body = response.bytes().await?;
583
584 if status.is_success() {
585 log::debug!("Received successful response. Read payload.");
586 let parsed_response = if status == http::StatusCode::NO_CONTENT
587 || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
588 {
589 serde_json::from_str("null")?
590 } else if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<String>() {
591 // Parse the output as a string.
592 let s = String::from_utf8(response_body.to_vec())?;
593 serde_json::from_value(serde_json::json!(&s))?
594 } else {
595 serde_json::from_slice::<Out>(&response_body)?
596 };
597 Ok(crate::Response::new(status, headers, parsed_response))
598 } else {
599 let error = if response_body.is_empty() {
600 ClientError::HttpError {
601 status,
602 headers,
603 error: "empty response".into(),
604 }
605 } else {
606 ClientError::HttpError {
607 status,
608 headers,
609 error: String::from_utf8_lossy(&response_body).into(),
610 }
611 };
612
613 Err(error)
614 }
615 }
616
617 /* TODO: make this more DRY */
618 #[allow(dead_code)]
619 async fn request_with_mime<Out>(
620 &self,
621 method: reqwest::Method,
622 uri: &str,
623 content: &[u8],
624 mime_type: &str,
625 ) -> ClientResult<crate::Response<Out>>
626 where
627 Out: serde::de::DeserializeOwned + 'static + Send,
628 {
629 let (url, auth) = self.url_and_auth(uri).await?;
630
631 let instance = <&Client>::clone(&self);
632
633 let mut req = instance.client.request(method, url);
634
635 // Set the default headers.
636 req = req.header(
637 reqwest::header::ACCEPT,
638 reqwest::header::HeaderValue::from_static("application/json"),
639 );
640 req = req.header(
641 reqwest::header::CONTENT_TYPE,
642 reqwest::header::HeaderValue::from_bytes(mime_type.as_bytes()).unwrap(),
643 );
644 // We are likely uploading a file so add the right headers.
645 req = req.header(
646 reqwest::header::HeaderName::from_static("x-upload-content-type"),
647 reqwest::header::HeaderValue::from_static("application/octet-stream"),
648 );
649 req = req.header(
650 reqwest::header::HeaderName::from_static("x-upload-content-length"),
651 reqwest::header::HeaderValue::from_bytes(format!("{}", content.len()).as_bytes())
652 .unwrap(),
653 );
654
655 if let Some(auth_str) = auth {
656 req = req.header(http::header::AUTHORIZATION, &*auth_str);
657 }
658
659 if content.len() > 1 {
660 let b = bytes::Bytes::copy_from_slice(content);
661 // We are uploading a file so add that as the body.
662 req = req.body(b);
663 }
664
665 let response = req.send().await?;
666
667 let status = response.status();
668 let headers = response.headers().clone();
669
670 let response_body = response.bytes().await?;
671
672 if status.is_success() {
673 log::debug!("Received successful response. Read payload.");
674 let parsed_response = if status == http::StatusCode::NO_CONTENT
675 || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
676 {
677 serde_json::from_str("null")?
678 } else {
679 serde_json::from_slice::<Out>(&response_body)?
680 };
681 Ok(crate::Response::new(status, headers, parsed_response))
682 } else {
683 let error = if response_body.is_empty() {
684 ClientError::HttpError {
685 status,
686 headers,
687 error: "empty response".into(),
688 }
689 } else {
690 ClientError::HttpError {
691 status,
692 headers,
693 error: String::from_utf8_lossy(&response_body).into(),
694 }
695 };
696
697 Err(error)
698 }
699 }
700
701 async fn request_entity<D>(
702 &self,
703 method: http::Method,
704 uri: &str,
705 message: Message,
706 ) -> ClientResult<crate::Response<D>>
707 where
708 D: serde::de::DeserializeOwned + 'static + Send,
709 {
710 let r = self.request(method, uri, message).await?;
711 Ok(r)
712 }
713
714 #[allow(dead_code)]
715 async fn get<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
716 where
717 D: serde::de::DeserializeOwned + 'static + Send,
718 {
719 self.request_entity(http::Method::GET, uri, message).await
720 }
721
722 #[allow(dead_code)]
723 async fn get_all_pages<D>(&self, uri: &str, _message: Message) -> ClientResult<Response<Vec<D>>>
724 where
725 D: serde::de::DeserializeOwned + 'static + Send,
726 {
727 // TODO: implement this.
728 self.unfold(uri).await
729 }
730
731 /// "unfold" paginated results of a vector of items
732 #[allow(dead_code)]
733 async fn unfold<D>(&self, uri: &str) -> ClientResult<crate::Response<Vec<D>>>
734 where
735 D: serde::de::DeserializeOwned + 'static + Send,
736 {
737 let mut global_items = Vec::new();
738 let (new_link, mut response) = self.get_pages(uri).await?;
739 let mut link = new_link;
740 while !response.body.is_empty() {
741 global_items.append(&mut response.body);
742 // We need to get the next link.
743 if let Some(url) = &link {
744 let url = reqwest::Url::parse(&url.0)?;
745 let (new_link, new_response) = self.get_pages_url(&url).await?;
746 link = new_link;
747 response = new_response;
748 }
749 }
750
751 Ok(Response::new(
752 response.status,
753 response.headers,
754 global_items,
755 ))
756 }
757
758 #[allow(dead_code)]
759 async fn get_pages<D>(
760 &self,
761 uri: &str,
762 ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
763 where
764 D: serde::de::DeserializeOwned + 'static + Send,
765 {
766 self.request_with_links(http::Method::GET, uri, Message::default())
767 .await
768 }
769
770 #[allow(dead_code)]
771 async fn get_pages_url<D>(
772 &self,
773 url: &reqwest::Url,
774 ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
775 where
776 D: serde::de::DeserializeOwned + 'static + Send,
777 {
778 self.request_with_links(http::Method::GET, url.as_str(), Message::default())
779 .await
780 }
781
782 #[allow(dead_code)]
783 async fn post<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
784 where
785 D: serde::de::DeserializeOwned + 'static + Send,
786 {
787 self.request_entity(http::Method::POST, uri, message).await
788 }
789
790 #[allow(dead_code)]
791 async fn patch<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
792 where
793 D: serde::de::DeserializeOwned + 'static + Send,
794 {
795 self.request_entity(http::Method::PATCH, uri, message).await
796 }
797
798 #[allow(dead_code)]
799 async fn put<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
800 where
801 D: serde::de::DeserializeOwned + 'static + Send,
802 {
803 self.request_entity(http::Method::PUT, uri, message).await
804 }
805
806 #[allow(dead_code)]
807 async fn delete<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
808 where
809 D: serde::de::DeserializeOwned + 'static + Send,
810 {
811 self.request_entity(http::Method::DELETE, uri, message)
812 .await
813 }
814
815 /// > Note: The orderId in the API response will not match the Id displayed in the ShipBob Merchant Portal when you navigate to the Orders page. ShipBob is currently undergoing a schema migration and the Id displayed in the ShipBob Merchant Portal is the shipmentId not the orderId. In the future, the portal will display both orderId(s) and shipmentId(s).
816 ///
817 /// Use the Orders Resource to create and retrieve orders in ShipBob.
818 ///
819 /// An order a digital record of a complete purchase that comes from an upstream source (i.e. Shopify) and is intended for ShipBob to fulfill. The order object includes products purchased, shipping address details, shipping method selected etc. Orders are created in ShipBob via a channel.
820 ///
821 /// When ShipBob fulfills the order, one or more shipments are created for that order. A shipment is a record of the physical package(s) sent out via a carrier. If an order is shipped in multiple packages then 2 or more shipments can be created for that order.
822 ///
823 /// ### Tips for creating orders in ShipBob via the POST Order endpoint:
824 ///
825 /// * Populate the referenceId with a unique and immutable order identifier from your upstream system. This field was created to allow you to tieback records in ShipBob with your upstream system.
826 ///
827 /// * Ensure that the <em> shipping method </em> passed in the API request matches exactly what the user has listed as the <em> shipping method </em> on the <em> Ship Option Mapping </em> setup page in the ShipBob Merchant Portal. If they don’t match, ShipBob will assume that the user wants to leverage ShipBob’s default shipping method.
828 ///
829 /// * You can leverage either productId (the ShipBob productId) or the product referenceId (your system's unique Identifier for products) when creating an order.
830 ///
831 /// Use the Shipments endpoints to retrieve fulfillment information for shipments or orders.
832 ///
833 /// A shipment is an object that is the result of a fulfillment of an order. An order can have one or more shipments. Say Shopify order #122323 contains 3 different products, shipped in two separate packages, there would be 2 shipments for that order.
834 ///
835 /// Serial numbers are unique identifiers for an individual item (e.g. your specific iPhone X that you bought at the Apple Store). No inventory item can possess duplicate serial numbers. Merchants can request “serial scan”, which means ShipBob will capture the serial number(s) upon sending a shipment so the merchant knows which customer received which individual item(s).
836 pub fn orders(&self) -> orders::Orders {
837 orders::Orders::new(self.clone())
838 }
839
840 /// Use the Products Resource to retrieve and create product records in ShipBob.
841 ///
842 /// A product is a virtual record created in ShipBob’s system via a channel. Say a merchant has two Shopify stores (each store would have its own channel), Kevin’s Shopify Store #133 and Kevin’s Shopify store #134. If the same SKU was sold on both stores, two products would be created for that SKU, one product would be created to represent the SKU sold on Store #133 and one to represent it on Store on #134, with productIds 3884009 and 3884008 respectively.
843 ///
844 /// While a product is a virtual record, the inventory item is a representation of a physical good. So in the above example, as product 3884009 and product 3884008 represent the same SKU sold on different channels, the same inventory item will be mapped to both products. Every product will have one or more inventory items mapped to it. Bundle products, a set of products that are sold to consumers as one combined package, think gift or multi packs, may have 2 or more inventory items mapped to them.
845 ///
846 /// ### Tips for creating products in ShipBob via the POST Product endpoints:
847 ///
848 /// * ShipBob needs products to be created at the lowest level. So if a product has 3 variants, small, medium and large, a separate product needs to be created in ShipBob for all three.
849 ///
850 /// * Populate the referenceId with a unique and immutable product identifier from your upstream system. This field was created to allow you to tie back records in ShipBob with your upstream system.
851 ///
852 /// * Use specific and/or unique names to describe each product so they can be easily identified by users in the ShipBob Merchant Portal. In particular, when creating variants, please give them distinguishable names i.e. for a Blue shirt that comes in two sizes, small and medium, strong product names would be Blue shirt size:small and Blue shirt size:medium.
853 ///
854 /// > **NOTE:** The productId returned in the API response will not match the id displayed in the ShipBob Merchant Portal when you navigate to Inventory > Products. ShipBob is currently undergoing a schema migration and the Id displayed in the ShipBob Merchant Portal is the inventoryId not the productId. In the future, the portal will display both productId(s) and inventoryId(s).
855 pub fn products(&self) -> products::Products {
856 products::Products::new(self.clone())
857 }
858
859 /// Use the Inventory Resource to retrieve ShipBob inventory items and quantities.
860 ///
861 /// An inventory item is a representation of a physical good, that may or may not have physical stock in ShipBob’s fulfillment centers. Every product will have one or more inventory items mapped to it. A bundle product (a set of products that are sold as one combined package - e.g. gift or multi packs) is an example of a product that has 2 or more inventory items mapped to it.
862 ///
863 /// Lot items are physical items that have expiration dates or batch numbers that should be fulfilled in a FIFO (first in, first out) manner. Most food items are lot items. Quantities by lot # and/or expiration date are also listed in the Inventory object.
864 pub fn inventory(&self) -> inventory::Inventory {
865 inventory::Inventory::new(self.clone())
866 }
867
868 /// Use the Channel Resource to list “channels” which you have access to. You will use this channelId for subsequent API calls made to ShipBob endpoints.
869 ///
870 /// A channel is a specific installation of an application built by a vendor on top of our API – e.g. Kevin’s Shopify Store #133432. All write and most read endpoints require a channel to be passed in the header to complete the request. The channel is used to Identify where the data originally came from.
871 ///
872 /// Applications that are granted multi-channel permissions will be able to read data from all channels that belong to a user. However, multi-channel applications will only be able to write on behalf of their own channel.
873 pub fn channels(&self) -> channels::Channels {
874 channels::Channels::new(self.clone())
875 }
876
877 /// **While the Returns API is live, ShipBob's end to end Returns process will not go live until the beginning of March. As a result, any returns arriving at ShipBob's fulfillment centers prior to March 12st, 2020 will NOT be processed**.
878 ///
879 /// Use the Returns resource to retrieve, create, edit and cancel return records in ShipBob.
880 ///
881 /// A return is a request for ShipBob to perform an action on inventory that is coming back into our fulfillment centers. Typically, the return is a result of an order being requested to be refunded or exchanged. ShipBob does not handle refunds or exchanges - we simply process the inventory according to the merchant specifications.
882 ///
883 /// Returns can only be **modified** or **cancelled** when they are in the Awaiting Arrival status. Returns that are being Processed or have been Completed cannot be modified or cancelled.
884 ///
885 /// ### Tips for creating return orders:
886 ///
887 /// * Populate the referenceId with a unique and immutable return identifier from your upstream system. This field was created to allow you to tie back records in ShipBob with your upstream system.
888 ///
889 /// *Include each inventoryId exactly once in the inventory object. If an inventoryId is included more than once, the call will return an error code
890 ///
891 /// * Provide a tracking # when submitting a return, while it is not a required field, it is the the most surefire way for ShipBob staff to properly and quickly identify the return package when it reaches our fulfillment center.
892 ///
893 /// * Only include inventory items to the return record that will be returned in the same box i.e. if InventoryId 12232 and InventoryId 12039 will be returned in two seperate boxes, two return orders should be created.
894 ///
895 /// * ShipBob does not process returns for digital items or bundle inventory items. Return calls that include digital inventory items (e.g. ebooks) or bundle inventory items (i.e. multipacks, combination of multiple inventory items) will return an error code.
896 ///
897 /// * If you choose to provide a requested action (it is an optional field), only provide one requested action per inventory item. So if you have more than 1 quantity of a given item being returned within the same box, all quantities of the item have to have the same action associated with them. If you don’t provide a requested action, it will default to the action the User set for that inventory item in the ShipBob Merchant portal.
898 pub fn returns(&self) -> returns::Returns {
899 returns::Returns::new(self.clone())
900 }
901
902 /// Use the Receiving Resource to retrieve, create and cancel Warehouse Receiving Orders (WROs).
903 ///
904 /// A WRO is a request form that tells ShipBob's fulfillment centers what inventory should be received and stocked. Some other solutions call this an “ASN” or Advanced Ship Notice. WROs may include multiple inventory items with specific quantities. More details on creating a WRO can be found [here](https://support.shipbob.com/s/article/New-Send-Inventory-to-ShipBob-WRO).
905 ///
906 /// A WRO can only be **canceled** if it is in the Awaiting status. WROs in Awaiting status are considered to still be in transit to ShipBob FCs. WROs that have Partially Arrived, have been Processed or are Completed, cannot be canceled.
907 pub fn receiving(&self) -> receiving::Receiving {
908 receiving::Receiving::new(self.clone())
909 }
910
911 /// Use the Webhooks Resource to create, view or delete subscriptions for a user.
912 pub fn webhooks(&self) -> webhooks::Webhooks {
913 webhooks::Webhooks::new(self.clone())
914 }
915
916 /// Use this API to interact with the physical locations across ShipBob's fulfillment network.
917 ///
918 /// An active ShipBob location is operational for fulfillment processes, including receiving inventory and processing returns. It's important to note that some locations, access is granted to all merchants by default, while some locations require special request for merchants to be granted access.
919 ///
920 /// For each location, determine if it can be leveraged for the user by viewing the access_granted & receiving_enabled fields.
921 pub fn locations(&self) -> locations::Locations {
922 locations::Locations::new(self.clone())
923 }
924}