From 5f77a90e1bbc976acf4d7f77120185e98e77f1e3 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 8 Jan 2021 16:03:19 +0100 Subject: [PATCH] remove boxed errors, some updates --- Cargo.toml | 3 +- README.md | 6 +-- src/errors.rs | 16 ++++++-- src/invoice.rs | 103 +++++++++++++++++++++++++------------------------ src/lib.rs | 97 ++++++++++++++++++++++++++++++++++++++++------ src/orders.rs | 61 +++++++++++++++-------------- 6 files changed, 185 insertions(+), 101 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6add406..33aeb14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "paypal-rs" -version = "0.2.0-alpha.2" +version = "0.2.0-alpha.3" authors = ["Edgar "] description = "A library that wraps the paypal api asynchronously." repository = "https://github.com/edg-l/paypal-rs/" @@ -23,6 +23,5 @@ log = "0.4.11" bytes = "1.0.0" [dev-dependencies] -# Can't update this until reqwest updates. tokio = { version = "1.0.1", features = ["macros", "rt"] } dotenv = "0.15.0" diff --git a/README.md b/README.md index d2274cb..843cb0e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # paypal-rs [![Crates.io](https://meritbadge.herokuapp.com/paypal-rs)](https://crates.io/crates/paypal-rs) ![Rust](https://github.com/edg-l/paypal-rs/workflows/Rust/badge.svg) -![Docs](https://docs.rs/paypal-rs/badge.svg) +[![Docs](https://docs.rs/paypal-rs/badge.svg)](https://docs.rs/paypal-rs) A rust library that wraps the [paypal api](https://developer.paypal.com/docs/api) asynchronously in a stringly typed manner. @@ -35,9 +35,7 @@ async fn main() { let order_payload = OrderPayload::new( Intent::Authorize, - vec![PurchaseUnit::new(Amount::new( - Currency::EUR, "10.0", - ))], + vec![PurchaseUnit::new(Amount::new(Currency::EUR, "10.0"))], ); let order = client diff --git a/src/errors.rs b/src/errors.rs index 81e04a2..ece14b7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,9 +5,10 @@ use std::collections::HashMap; use std::error::Error; use std::fmt; + /// A paypal api response error. #[derive(Debug, Serialize, Deserialize)] -pub struct ApiResponseError { +pub struct PaypalError { /// The error name. pub name: String, /// The error message. @@ -24,13 +25,22 @@ pub struct ApiResponseError { pub links: Vec, } -impl fmt::Display for ApiResponseError { +impl fmt::Display for PaypalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:#?}", self) } } -impl Error for ApiResponseError {} +impl Error for PaypalError {} + +/// A response error, it may be paypal related or an error related to the http request itself. +#[derive(Debug)] +pub enum ResponseError { + /// A paypal api error. + ApiError(PaypalError), + /// A http error. + HttpError(reqwest::Error) +} /// When a currency is invalid. #[derive(Debug)] diff --git a/src/invoice.rs b/src/invoice.rs index 224e5a0..da8539e 100644 --- a/src/invoice.rs +++ b/src/invoice.rs @@ -7,7 +7,8 @@ //! Reference: https://developer.paypal.com/docs/api/invoicing/v2/ use crate::common::*; -use crate::errors; +use crate::HeaderParams; +use crate::errors::{ResponseError, PaypalError}; use bytes::Bytes; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -727,7 +728,7 @@ impl super::Client { pub async fn generate_invoice_number( &mut self, header_params: crate::HeaderParams, - ) -> Result> { + ) -> Result { let build = self .setup_headers( self.client @@ -736,13 +737,13 @@ impl super::Client { ) .await; - let res = build.send().await?; + let res = build.send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let x = res.json::>().await?; + let x = res.json::>().await.map_err(ResponseError::HttpError)?; Ok(x.get("invoice_number").expect("to have a invoice number").clone()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } @@ -751,8 +752,8 @@ impl super::Client { pub async fn create_draft_invoice( &mut self, invoice: InvoicePayload, - header_params: crate::HeaderParams, - ) -> Result> { + header_params: HeaderParams, + ) -> Result { let build = self .setup_headers( self.client @@ -761,22 +762,22 @@ impl super::Client { ) .await; - let res = build.json(&invoice).send().await?; + let res = build.json(&invoice).send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let x = res.json::().await?; + let x = res.json::().await.map_err(ResponseError::HttpError)?; Ok(x) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Get an invoice by ID. - pub async fn get_invoice( + pub async fn get_invoice( &mut self, - invoice_id: S, - header_params: crate::HeaderParams, - ) -> Result> { + invoice_id: &str, + header_params: HeaderParams, + ) -> Result { let build = self .setup_headers( self.client @@ -785,13 +786,13 @@ impl super::Client { ) .await; - let res = build.send().await?; + let res = build.send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let x = res.json::().await?; + let x = res.json::().await.map_err(ResponseError::HttpError)?; Ok(x) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } @@ -801,8 +802,8 @@ impl super::Client { &mut self, page: i32, page_size: i32, - header_params: crate::HeaderParams, - ) -> Result> { + header_params: HeaderParams, + ) -> Result { let build = self .setup_headers( self.client.get( @@ -818,22 +819,22 @@ impl super::Client { ) .await; - let res = build.send().await?; + let res = build.send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let x = res.json::().await?; + let x = res.json::().await.map_err(ResponseError::HttpError)?; Ok(x) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Delete a invoice - pub async fn delete_invoice( + pub async fn delete_invoice( &mut self, - invoice_id: S, - header_params: crate::HeaderParams, - ) -> Result<(), Box> { + invoice_id: &str, + header_params: HeaderParams, + ) -> Result<(), ResponseError> { let build = self .setup_headers( self.client @@ -842,12 +843,12 @@ impl super::Client { ) .await; - let res = build.send().await?; + let res = build.send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { Ok(()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } @@ -857,8 +858,8 @@ impl super::Client { invoice: Invoice, send_to_recipient: bool, send_to_invoicer: bool, - header_params: crate::HeaderParams, - ) -> Result<(), Box> { + header_params: HeaderParams, + ) -> Result<(), ResponseError> { let build = self .setup_headers( self.client.put( @@ -875,22 +876,22 @@ impl super::Client { ) .await; - let res = build.send().await?; + let res = build.send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { Ok(()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Cancel a invoice - pub async fn cancel_invoice( + pub async fn cancel_invoice( &mut self, - invoice_id: S, + invoice_id: &str, reason: CancelReason, - header_params: crate::HeaderParams, - ) -> Result<(), Box> { + header_params: HeaderParams, + ) -> Result<(), ResponseError> { let build = self .setup_headers( self.client @@ -899,22 +900,22 @@ impl super::Client { ) .await; - let res = build.json(&reason).send().await?; + let res = build.json(&reason).send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { Ok(()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Generate a QR code - pub async fn generate_qr_code( + pub async fn generate_qr_code( &mut self, - invoice_id: S, + invoice_id: &str, params: QRCodeParams, - header_params: crate::HeaderParams, - ) -> Result> { + header_params: HeaderParams, + ) -> Result { let build = self .setup_headers( self.client.post( @@ -929,23 +930,23 @@ impl super::Client { ) .await; - let res = build.json(¶ms).send().await?; + let res = build.json(¶ms).send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let b = res.bytes().await?; + let b = res.bytes().await.map_err(ResponseError::HttpError)?; Ok(b) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Records a payment for the invoice. If no payment is due, the invoice is marked as PAID. Otherwise, the invoice is marked as PARTIALLY PAID. - pub async fn record_invoice_payment( + pub async fn record_invoice_payment( &mut self, - invoice_id: S, + invoice_id: &str, payload: RecordPaymentPayload, header_params: crate::HeaderParams, - ) -> Result> { + ) -> Result { let build = self .setup_headers( self.client @@ -954,13 +955,13 @@ impl super::Client { ) .await; - let res = build.json(&payload).send().await?; + let res = build.json(&payload).send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let x = res.json::>().await?; + let x = res.json::>().await.map_err(ResponseError::HttpError)?; Ok(x.get("payment_id").unwrap().to_owned()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } diff --git a/src/lib.rs b/src/lib.rs index 80bee75..163e6da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,82 @@ -//! # paypal-rs +//! [![Crates.io](https://meritbadge.herokuapp.com/paypal-rs)](https://crates.io/crates/paypal-rs) //! ![Rust](https://github.com/edg-l/paypal-rs/workflows/Rust/badge.svg) -//! ![Docs](https://docs.rs/paypal-rs/badge.svg) +//! [![Docs](https://docs.rs/paypal-rs/badge.svg)](https://docs.rs/paypal-rs) //! -//! A rust library that wraps the [paypal api](https://developer.paypal.com/docs/api) asynchronously in a strongly typed manner. +//! A rust library that wraps the [paypal api](https://developer.paypal.com/docs/api) asynchronously in a stringly typed manner. //! //! Crate: https://crates.io/crates/paypal-rs //! //! Documentation: https://docs.rs/paypal-rs //! //! Currently in early development. +//! + +//! ## Example +//! +//! ```rust +//! use paypal_rs::{ +//! Client, +//! HeaderParams, +//! Prefer, +//! orders::{OrderPayload, Intent, PurchaseUnit, Amount}, +//! common::Currency, +//! }; +//! +//! #[tokio::main] +//! async fn main() { +//! dotenv::dotenv::ok(); +//! let clientid = std::env::var("PAYPAL_CLIENTID").unwrap(); +//! let secret = std::env::var("PAYPAL_SECRET").unwrap(); +//! +//! let mut client = Client::new(clientid, secret, true); +//! +//! client.get_access_token().await.unwrap(); +//! +//! let order_payload = OrderPayload::new( +//! Intent::Authorize, +//! vec![PurchaseUnit::new(Amount::new(Currency::EUR, "10.0"))], +//! ); +//! +//! let order = client +//! .create_order( +//! order_payload, +//! HeaderParams { +//! prefer: Some(Prefer::Representation), +//! ..Default::default() +//! }, +//! ) +//! .await +//! .unwrap(); +//! } +//! ``` +//! +//! ## Testing +//! You need the enviroment variables PAYPAL_CLIENTID and PAYPAL_SECRET to be set. +//! +//! `cargo test` +//! +//! ## Roadmap +//! +//! - [x] Orders API - 0.1.0 +//! - - [x] Create order +//! - - [x] Update order +//! - - [x] Show order details +//! - - [x] Authorize payment for order +//! - - [x] Capture payment for order +//! - [ ] Invoicing API - 0.2.0 +//! - [ ] Payments API - 0.3.0 +//! - [ ] Tracking API - 0.4.0 +//! - [ ] Subscriptions API - 0.5.0 +//! - [ ] Identity API - 0.6.0 +//! - [ ] Disputes API - 0.7.0 +//! - [ ] Catalog Products API - 0.8.0 +//! - [ ] Partner Referrals API - 0.9.0 +//! - [ ] Payouts API - 0.10.0 +//! - [ ] Transaction Search API - 0.11.0 +//! - [ ] Referenced Payouts API - 0.12.0 +//! - [ ] Vault API - 0.13.0 +//! - [ ] Webhooks Management API - 0.14.0 +//! - [ ] Payment Experience Web Profiles API - 1.0.0 #![deny(missing_docs)] @@ -17,6 +85,7 @@ pub mod errors; pub mod invoice; pub mod orders; +use errors::{PaypalError, ResponseError}; use reqwest::header; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; @@ -271,7 +340,7 @@ impl Client { } /// Gets a access token used in all the api calls. - pub async fn get_access_token(&mut self) -> Result<(), Box> { + pub async fn get_access_token(&mut self) -> Result<(), ResponseError> { if !self.access_token_expired() { return Ok(()); } @@ -283,15 +352,18 @@ impl Client { .header("Accept", "application/json") .body("grant_type=client_credentials") .send() - .await?; + .await + .map_err(ResponseError::HttpError)?; if res.status().is_success() { - let token = res.json::().await?; + let token = res.json::().await.map_err(ResponseError::HttpError)?; self.auth.expires = Some((Instant::now(), Duration::new(token.expires_in, 0))); self.auth.access_token = Some(token); Ok(()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError( + res.json::().await.map_err(ResponseError::HttpError)?, + )) } } @@ -307,10 +379,10 @@ impl Client { #[cfg(test)] mod tests { - use crate::{orders::*, Client, HeaderParams, Prefer}; use crate::common::Currency; - use std::str::FromStr; + use crate::{orders::*, Client, HeaderParams, Prefer}; use std::env; + use std::str::FromStr; async fn create_client() -> Client { dotenv::dotenv().ok(); @@ -326,7 +398,10 @@ mod tests { async fn test_order() { let mut client = create_client().await; - let order = OrderPayload::new(Intent::Authorize, vec![PurchaseUnit::new(Amount::new(Currency::EUR, "10.0"))]); + let order = OrderPayload::new( + Intent::Authorize, + vec![PurchaseUnit::new(Amount::new(Currency::EUR, "10.0"))], + ); let ref_id = format!( "TEST-{:?}", @@ -354,7 +429,7 @@ mod tests { client .update_order( - order_created.id, + &order_created.id, Some(Intent::Capture), Some(order_created.purchase_units.expect("to exist")), ) diff --git a/src/orders.rs b/src/orders.rs index c05a687..14d21f8 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -4,8 +4,9 @@ //! //! Reference: https://developer.paypal.com/docs/api/orders/v2/ +use crate::HeaderParams; use crate::common::*; -use crate::errors; +use crate::errors::{ResponseError, PaypalError}; use serde::{Deserialize, Serialize}; /// The intent to either capture payment immediately or authorize a payment for an order after order creation. @@ -742,8 +743,8 @@ impl super::Client { pub async fn create_order( &mut self, order: OrderPayload, - header_params: crate::HeaderParams, - ) -> Result> { + header_params: HeaderParams, + ) -> Result { let builder = { self.setup_headers( self.client.post(&format!("{}/v2/checkout/orders", self.endpoint())), @@ -751,24 +752,24 @@ impl super::Client { ) .await }; - let res = builder.json(&order).send().await?; + let res = builder.json(&order).send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let order = res.json::().await?; + let order = res.json::().await.map_err(ResponseError::HttpError)?; Ok(order) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Used internally for order requests that have no body. - async fn build_endpoint_order( + async fn build_endpoint_order( &mut self, - order_id: S, - endpoint: A, + order_id: &str, + endpoint: &str, post: bool, header_params: crate::HeaderParams, - ) -> Result> { + ) -> Result { let format = format!("{}/v2/checkout/orders/{}/{}", self.endpoint(), order_id, endpoint); let builder = self @@ -781,13 +782,13 @@ impl super::Client { ) .await; - let res = builder.send().await?; + let res = builder.send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { - let order = res.json::().await?; + let order = res.json::().await.expect("error serializing json response"); Ok(order) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } @@ -799,12 +800,12 @@ impl super::Client { /// Note: You can only update the intent from Authorize to Capture /// /// More info on what you can change: https://developer.paypal.com/docs/api/orders/v2/#orders_patch - pub async fn update_order( + pub async fn update_order( &mut self, - id: S, + id: &str, intent: Option, purchase_units: Option>, - ) -> Result<(), Box> { + ) -> Result<(), ResponseError> { let mut intent_json = String::new(); let units_json = String::new(); @@ -812,7 +813,7 @@ impl super::Client { let mut units_json = String::new(); for (i, unit) in p_units.iter().enumerate() { - let unit_str = serde_json::to_string(&unit)?; + let unit_str = serde_json::to_string(&unit).expect("error deserializing PurchaseUnit json"); let mut unit_json = format!( r#" {{ @@ -871,32 +872,32 @@ impl super::Client { .await }; - let res = builder.body(final_json.clone()).send().await?; + let res = builder.body(final_json.clone()).send().await.map_err(ResponseError::HttpError)?; if res.status().is_success() { Ok(()) } else { - Err(Box::new(res.json::().await?)) + Err(ResponseError::ApiError(res.json::().await.map_err(ResponseError::HttpError)?)) } } /// Shows details for an order, by ID. - pub async fn show_order_details( + pub async fn show_order_details( &mut self, - order_id: S, - ) -> Result> { - self.build_endpoint_order(order_id, "", false, crate::HeaderParams::default()) + order_id: &str, + ) -> Result { + self.build_endpoint_order(order_id, "", false, HeaderParams::default()) .await } /// Captures payment for an order. To successfully capture payment for an order, /// the buyer must first approve the order or a valid payment_source must be provided in the request. /// A buyer can approve the order upon being redirected to the rel:approve URL that was returned in the HATEOAS links in the create order response. - pub async fn capture_order( + pub async fn capture_order( &mut self, - order_id: S, + order_id: &str, header_params: crate::HeaderParams, - ) -> Result> { + ) -> Result { self.build_endpoint_order(order_id, "capture", true, header_params) .await } @@ -904,11 +905,11 @@ impl super::Client { /// Authorizes payment for an order. To successfully authorize payment for an order, /// the buyer must first approve the order or a valid payment_source must be provided in the request. /// A buyer can approve the order upon being redirected to the rel:approve URL that was returned in the HATEOAS links in the create order response. - pub async fn authorize_order( + pub async fn authorize_order( &mut self, - order_id: S, - header_params: crate::HeaderParams, - ) -> Result> { + order_id: &str, + header_params: HeaderParams, + ) -> Result { self.build_endpoint_order(order_id, "authorize", true, header_params) .await }