date filters strongly typed; formatting

This commit is contained in:
Aaron Manning 2025-02-13 15:27:44 +11:00
parent c8b91f66db
commit 9eeee510a5
11 changed files with 873 additions and 447 deletions

View File

@ -13,3 +13,4 @@ url = "2.2.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
chrono = "0.4.39"

View File

@ -7,71 +7,76 @@ use serde::Deserialize;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ListAccountsResponse { pub struct ListAccountsResponse {
/// The list of accounts returned in this response. /// The list of accounts returned in this response.
pub data : Vec<AccountResource>, pub data: Vec<AccountResource>,
pub links : ResponseLinks, pub links: ResponseLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct GetAccountResponse { pub struct GetAccountResponse {
/// The account returned in this response. /// The account returned in this response.
pub data : AccountResource, pub data: AccountResource,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct AccountResource { pub struct AccountResource {
/// The type of this resource: `accounts`. /// The type of this resource: `accounts`.
pub r#type : String, pub r#type: String,
/// The unique identifier for this account. /// The unique identifier for this account.
pub id : String, pub id: String,
pub attributes : Attributes, pub attributes: Attributes,
pub relationships : Relationships, pub relationships: Relationships,
pub links : Option<AccountResourceLinks>, pub links: Option<AccountResourceLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct AccountResourceLinks { pub struct AccountResourceLinks {
/// The canonical link to this resource within the API. /// The canonical link to this resource within the API.
#[serde(rename = "self")] #[serde(rename = "self")]
pub this : Option<String>, pub this: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ResponseLinks { pub struct ResponseLinks {
/// The link to the previous page in the results. If this value is `None` there is no previous page. /// The link to the previous page in the results. If this value is `None`
pub prev : Option<String>, /// there is no previous page.
/// The link to the next page in the results. If this value is `None` there is no next page. pub prev: Option<String>,
pub next : Option<String>, /// The link to the next page in the results. If this value is `None` there
/// is no next page.
pub next: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Relationships { pub struct Relationships {
pub transactions : Transactions, pub transactions: Transactions,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Transactions { pub struct Transactions {
pub links : Option<TransactionLinks>, pub links: Option<TransactionLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransactionLinks { pub struct TransactionLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Attributes { pub struct Attributes {
/// The name associated with the account in the Up application. /// The name associated with the account in the Up application.
pub display_name : String, pub display_name: String,
/// The bank account type of this account. Possible values: SAVER, TRANSACTIONAL /// The bank account type of this account. Possible values: SAVER,
pub account_type : AccountType, /// TRANSACTIONAL
/// The ownership structure for this account. Possible values: INDIVIDUAL, JOINT pub account_type: AccountType,
pub ownership_type : OwnershipType, /// The ownership structure for this account. Possible values: INDIVIDUAL,
/// The available balance of the account, taking into account any amounts that are currently on hold. /// JOINT
pub balance : standard::MoneyObject, pub ownership_type: OwnershipType,
/// The available balance of the account, taking into account any amounts
/// that are currently on hold.
pub balance: standard::MoneyObject,
/// The date-time at which this account was first opened. /// The date-time at which this account was first opened.
pub created_at : String, pub created_at: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -93,30 +98,32 @@ pub enum OwnershipType {
#[derive(Default)] #[derive(Default)]
pub struct ListAccountsOptions { pub struct ListAccountsOptions {
/// The number of records to return in each page. /// The number of records to return in each page.
page_size : Option<u8>, page_size: Option<u8>,
/// The type of account for which to return records. This can be used to filter Savers from spending accounts. /// The type of account for which to return records. This can be used to
filter_account_type : Option<String>, /// filter Savers from spending accounts.
/// The account ownership structure for which to return records. This can be used to filter 2Up accounts from Up accounts. filter_account_type: Option<String>,
filter_ownership_type : Option<String>, /// The account ownership structure for which to return records. This can
/// be used to filter 2Up accounts from Up accounts.
filter_ownership_type: Option<String>,
} }
impl ListAccountsOptions { impl ListAccountsOptions {
/// Sets the page size. /// Sets the page size.
pub fn page_size(&mut self, value : u8) { pub fn page_size(&mut self, value: u8) {
self.page_size = Some(value); self.page_size = Some(value);
} }
/// Sets the account type filter value. /// Sets the account type filter value.
pub fn filter_account_type(&mut self, value : String) { pub fn filter_account_type(&mut self, value: String) {
self.filter_account_type = Some(value); self.filter_account_type = Some(value);
} }
/// Sets the ownership type filter value. /// Sets the ownership type filter value.
pub fn filter_ownership_type(&mut self, value : String) { pub fn filter_ownership_type(&mut self, value: String) {
self.filter_ownership_type = Some(value); self.filter_ownership_type = Some(value);
} }
fn add_params(&self, url : &mut reqwest::Url) { fn add_params(&self, url: &mut reqwest::Url) {
let mut query = String::new(); let mut query = String::new();
if let Some(value) = &self.page_size { if let Some(value) = &self.page_size {
@ -130,14 +137,18 @@ impl ListAccountsOptions {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[accountType]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[accountType]={}", urlencoding::encode(value))
);
} }
if let Some(value) = &self.filter_ownership_type { if let Some(value) = &self.filter_ownership_type {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[ownershipType]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[ownershipType]={}", urlencoding::encode(value))
);
} }
if !query.is_empty() { if !query.is_empty() {
@ -147,9 +158,16 @@ impl ListAccountsOptions {
} }
impl Client { impl Client {
/// Retrieve a paginated list of all accounts for the currently authenticated user. The returned list is paginated and can be scrolled by following the `prev` and `next` links where present. /// Retrieve a paginated list of all accounts for the currently
pub async fn list_accounts(&self, options : &ListAccountsOptions) -> Result<ListAccountsResponse, error::Error> { /// authenticated user. The returned list is paginated and can be scrolled
let mut url = reqwest::Url::parse(&format!("{}/accounts", BASE_URL)).map_err(error::Error::UrlParse)?; /// by following the `prev` and `next` links where present.
pub async fn list_accounts(
&self,
options: &ListAccountsOptions,
) -> Result<ListAccountsResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/accounts", BASE_URL)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -162,13 +180,15 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let account_response : ListAccountsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let account_response: ListAccountsResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Ok(account_response) Ok(account_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
@ -176,14 +196,20 @@ impl Client {
} }
/// Retrieve a specific account by providing its unique identifier. /// Retrieve a specific account by providing its unique identifier.
pub async fn get_account(&self, id : &str) -> Result<GetAccountResponse, error::Error> { pub async fn get_account(
// This assertion is because without an ID the request is thought to be a request for &self,
// many accounts, and therefore the error messages are very unclear. id: &str,
) -> Result<GetAccountResponse, error::Error> {
// This assertion is because without an ID the request is thought to be
// a request for many accounts, and therefore the error messages are
// very unclear.
if id.is_empty() { if id.is_empty() {
panic!("The provided account ID must not be empty."); panic!("The provided account ID must not be empty.");
} }
let url = reqwest::Url::parse(&format!("{}/accounts/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; let url = reqwest::Url::parse(
&format!("{}/accounts/{}", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(url) .get(url)
@ -195,13 +221,15 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let account_response : GetAccountResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let account_response: GetAccountResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Ok(account_response) Ok(account_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }

View File

@ -7,108 +7,112 @@ use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ListCategoriesResponse { pub struct ListCategoriesResponse {
/// The list of categories returned in this response. /// The list of categories returned in this response.
pub data : Vec<CategoryResource>, pub data: Vec<CategoryResource>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct GetCategoryResponse { pub struct GetCategoryResponse {
/// The category returned in this response. /// The category returned in this response.
pub data : CategoryResource, pub data: CategoryResource,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CategoryResource { pub struct CategoryResource {
/// The type of this resource: categories /// The type of this resource: categories
pub r#type : String, pub r#type: String,
/// The unique identifier for this category. This is a human-readable but URL-safe value. /// The unique identifier for this category. This is a human-readable but
pub id : String, /// URL-safe value.
pub attributes : Attributes, pub id: String,
pub relationships : Relationships, pub attributes: Attributes,
pub links : Option<CategoryResourceLinks>, pub relationships: Relationships,
pub links: Option<CategoryResourceLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Attributes { pub struct Attributes {
/// The name of this category as seen in the Up application. /// The name of this category as seen in the Up application.
pub name : String, pub name: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Relationships { pub struct Relationships {
pub parent : Parent, pub parent: Parent,
pub children : Children, pub children: Children,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Parent { pub struct Parent {
pub data : Option<ParentData>, pub data: Option<ParentData>,
pub links : Option<ParentLinks>, pub links: Option<ParentLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ParentData { pub struct ParentData {
/// The type of this resource: `categories` /// The type of this resource: `categories`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ParentLinks { pub struct ParentLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Children { pub struct Children {
pub data : Vec<ChildrenData>, pub data: Vec<ChildrenData>,
pub links : Option<ChildrenLinks>, pub links: Option<ChildrenLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ChildrenData { pub struct ChildrenData {
/// The type of this resource: `categories` /// The type of this resource: `categories`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ChildrenLinks { pub struct ChildrenLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CategoryResourceLinks { pub struct CategoryResourceLinks {
/// The canonical link to this resource within the API. /// The canonical link to this resource within the API.
#[serde(rename = "self")] #[serde(rename = "self")]
pub this : String, pub this: String,
} }
// ----------------- Input Objects ----------------- // ----------------- Input Objects -----------------
#[derive(Default)] #[derive(Default)]
pub struct ListCategoriesOptions { pub struct ListCategoriesOptions {
/// The unique identifier of a parent category for which to return only its children. /// The unique identifier of a parent category for which to return only its
filter_parent : Option<String>, /// children.
filter_parent: Option<String>,
} }
impl ListCategoriesOptions { impl ListCategoriesOptions {
/// Sets the parent filter value. /// Sets the parent filter value.
pub fn filter_parent(&mut self, value : String) { pub fn filter_parent(&mut self, value: String) {
self.filter_parent = Some(value); self.filter_parent = Some(value);
} }
fn add_params(&self, url : &mut reqwest::Url) { fn add_params(&self, url: &mut reqwest::Url) {
let mut query = String::new(); let mut query = String::new();
if let Some(value) = &self.filter_parent { if let Some(value) = &self.filter_parent {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[parent]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[parent]={}", urlencoding::encode(value))
);
} }
if !query.is_empty() { if !query.is_empty() {
@ -121,22 +125,30 @@ impl ListCategoriesOptions {
#[derive(Serialize)] #[derive(Serialize)]
struct CategoriseTransactionRequest { struct CategoriseTransactionRequest {
/// The category to set on the transaction. Set this entire key to `null` de-categorize a transaction. /// The category to set on the transaction. Set this entire key to `null`
data : Option<CategoryInputResourceIdentifier>, /// de-categorize a transaction.
data: Option<CategoryInputResourceIdentifier>,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct CategoryInputResourceIdentifier { struct CategoryInputResourceIdentifier {
/// The type of this resource: `categories` /// The type of this resource: `categories`
r#type : String, r#type: String,
/// The unique identifier of the category, as returned by the `list_categories` method. /// The unique identifier of the category, as returned by the
id : String, /// `list_categories` method.
id: String,
} }
impl Client { impl Client {
/// Retrieve a list of all categories and their ancestry. The returned list is not paginated. /// Retrieve a list of all categories and their ancestry. The returned list
pub async fn list_categories(&self, options : &ListCategoriesOptions) -> Result<ListCategoriesResponse, error::Error> { /// is not paginated.
let mut url = reqwest::Url::parse(&format!("{}/categories", BASE_URL)).map_err(error::Error::UrlParse)?; pub async fn list_categories(
&self,
options: &ListCategoriesOptions,
) -> Result<ListCategoriesResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/categories", BASE_URL)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -149,13 +161,15 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let category_response : ListCategoriesResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let category_response: ListCategoriesResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Ok(category_response) Ok(category_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
@ -163,14 +177,19 @@ impl Client {
} }
/// Retrieve a specific category by providing its unique identifier. /// Retrieve a specific category by providing its unique identifier.
pub async fn get_category(&self, id : &str) -> Result<GetCategoryResponse, error::Error> { pub async fn get_category(
// This assertion is because without an ID the request is thought to be a request for &self, id: &str,
// many categories, and therefore the error messages are very unclear. ) -> Result<GetCategoryResponse, error::Error> {
// This assertion is because without an ID the request is thought to be
// a request for many categories, and therefore the error messages are
// very unclear.
if id.is_empty() { if id.is_empty() {
panic!("The provided category ID must not be empty."); panic!("The provided category ID must not be empty.");
} }
let url = reqwest::Url::parse(&format!("{}/categories/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; let url = reqwest::Url::parse(
&format!("{}/categories/{}", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(url) .get(url)
@ -182,32 +201,51 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let category_response : GetCategoryResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let category_response: GetCategoryResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Ok(category_response ) Ok(category_response )
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Updates the category associated with a transaction. Only transactions for which `is_categorizable` is set to true support this operation. The `id` is taken from the list exposed on `list_categories` and cannot be one of the top-level (parent) categories. To de-categorize a transaction, set the entire `data` key to `null`. The associated category, along with its request URL is also exposed via the category relationship on the transaction resource returned from `get_transaction`. /// Updates the category associated with a transaction. Only transactions
pub async fn categorise_transaction(&self, transaction_id : &str, category : Option<&str>) -> Result<(), error::Error> { /// for which `is_categorizable` is set to true support this operation. The
let url = reqwest::Url::parse(&format!("{}/transactions/{}/relationships/category", BASE_URL, transaction_id)).map_err(error::Error::UrlParse)?; /// `id` is taken from the list exposed on `list_categories` and cannot be
/// one of the top-level (parent) categories. To de-categorize a
/// transaction, set the entire `data` key to `null`. The associated
/// category, along with its request URL is also exposed via the category
/// relationship on the transaction resource returned from `get_transaction`.
pub async fn categorise_transaction(
&self,
transaction_id: &str,
category: Option<&str>,
) -> Result<(), error::Error> {
let url = reqwest::Url::parse(
&format!("{}/transactions/{}/relationships/category",
BASE_URL,
transaction_id,
)
).map_err(error::Error::UrlParse)?;
let category = category.map(|id| { let category = category.map(|id| {
CategoryInputResourceIdentifier { CategoryInputResourceIdentifier {
r#type : String::from("categories"), r#type: String::from("categories"),
id : String::from(id), id: String::from(id),
} }
}); });
let body = CategoriseTransactionRequest { data : category }; let body = CategoriseTransactionRequest { data: category };
let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?; let body =
serde_json::to_string(&body)
.map_err(error::Error::Serialize)?;
println!("{}", body); println!("{}", body);
@ -226,7 +264,8 @@ impl Client {
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }

View File

@ -11,26 +11,44 @@ pub enum Error {
Request(reqwest::Error), Request(reqwest::Error),
/// Represents errors from the API (i.e. a non `2XX` response code). /// Represents errors from the API (i.e. a non `2XX` response code).
Api(ErrorResponse), Api(ErrorResponse),
/// Represents an error in deserializing JSON to the required structures. Occurances of this /// Represents an error in deserializing JSON to the required structures.
/// error should be treated as a bug in the library. /// Occurences of this error should be treated as a bug in the library.
Json(serde_json::Error), Json(serde_json::Error),
/// Represents an error in reading the body from the HTTP response. Occurances of this /// Represents an error in reading the body from the HTTP response.
/// error should be treated as a bug in the library. /// Occurences of this error should be treated as a bug in the library.
BodyRead(reqwest::Error), BodyRead(reqwest::Error),
/// Represents an error serializing the data to be sent to the API. Occurances of this /// Represents an error serializing the data to be sent to the API.
/// error should be treated as a bug in the library. /// Occurences of this error should be treated as a bug in the library.
Serialize(serde_json::Error), Serialize(serde_json::Error),
} }
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f : &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self { match self {
Self::UrlParse(val) => write!(f, "Failed to parse the URL before making the request: {:?}", val), Self::UrlParse(val) => write!(f,
Self::Request(val) => write!(f, "Failed to make the HTTP request to the API endpoint: {:?}", val), "Failed to parse the URL before making the request: {:?}",
Self::Api(val) => write!(f, "The API returned an error response: {:?}", val), val,
Self::Json(val) => write!(f, "Failed to deserialize the returned JSON to the correct format: {:?}", val), ),
Self::BodyRead(val) => write!(f, "Failed to read the response body as a UTF-8 string: {:?}", val), Self::Request(val) => write!(f,
Self::Serialize(val) => write!(f, "Failed to serialize the request data: {:?}", val), "Failed to make the HTTP request to the API endpoint: {:?}",
val,
),
Self::Api(val) => write!(f,
"The API returned an error response: {:?}",
val,
),
Self::Json(val) => write!(f,
"Failed to deserialize the returned JSON to the correct format: {:?}",
val,
),
Self::BodyRead(val) => write!(f,
"Failed to read the response body as a UTF-8 string: {:?}",
val,
),
Self::Serialize(val) => write!(f,
"Failed to serialize the request data: {:?}",
val,
),
} }
} }
} }
@ -40,25 +58,33 @@ impl std::error::Error for Error {}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ErrorResponse { pub struct ErrorResponse {
/// The list of errors returned in this response. /// The list of errors returned in this response.
pub errors : Vec<ErrorObject>, pub errors: Vec<ErrorObject>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ErrorObject { pub struct ErrorObject {
/// The HTTP status code associated with this error. The status indicates the broad type of error according to HTTP semantics. /// The HTTP status code associated with this error. The status indicates
pub status : String, /// the broad type of error according to HTTP semantics.
/// A short description of this error. This should be stable across multiple occurrences of this type of error and typically expands on the reason for the status code. pub status: String,
pub title : String, /// A short description of this error. This should be stable across
/// A detailed description of this error. This should be considered unique to individual occurrences of an error and subject to change. It is useful for debugging purposes. /// multiple occurrences of this type of error and typically expands on the
pub detail : String, /// reason for the status code.
/// If applicable, location in the request that this error relates to. This may be a parameter in the query string, or a an attribute in the request body. pub title: String,
pub source : Option<Source>, /// A detailed description of this error. This should be considered unique
/// to individual occurrences of an error and subject to change. It is
/// useful for debugging purposes.
pub detail: String,
/// If applicable, location in the request that this error relates to. This
/// may be a parameter in the query string, or a an attribute in the
/// request body.
pub source: Option<Source>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Source { pub struct Source {
/// If this error relates to a query parameter, the name of the parameter. /// If this error relates to a query parameter, the name of the parameter.
pub parameter : Option<String>, pub parameter: Option<String>,
/// If this error relates to an attribute in the request body, a rfc-6901 JSON pointer to the attribute. /// If this error relates to an attribute in the request body, a rfc-6901
pub pointer : Option<String> /// JSON pointer to the attribute.
pub pointer: Option<String>
} }

View File

@ -1,7 +1,10 @@
macro_rules! implement_pagination_v1 { macro_rules! implement_pagination_v1 {
($t:ty) => { ($t:ty) => {
impl $t { impl $t {
async fn follow_link(client : &Client, url : &str) -> Result<Self, error::Error> { async fn follow_link(
client: &Client,
url: &str,
) -> Result<Self, error::Error> {
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(url) .get(url)
.header("Authorization", client.auth_header()) .header("Authorization", client.auth_header())
@ -11,22 +14,36 @@ macro_rules! implement_pagination_v1 {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let response : Self = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let response: Self =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(response) Ok(response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Follows the link to the next page, returns None of the next page does not exist. /// Follows the link to the next page, returns None of the next
pub async fn next(&self, client : &Client) -> Option<Result<Self, error::Error>> { /// page does not exist.
pub async fn next(
&self,
client: &Client,
) -> Option<Result<Self, error::Error>> {
match match
self self
.links .links
@ -39,8 +56,11 @@ macro_rules! implement_pagination_v1 {
} }
} }
/// Follows the link to the previous page, returns None of the previous page does not exist. /// Follows the link to the previous page, returns None of the
pub async fn prev(&self, client : &Client) -> Option<Result<Self, error::Error>> { /// previous page does not exist.
pub async fn prev(
&self, client: &Client,
) -> Option<Result<Self, error::Error>> {
match match
self self
.links .links

View File

@ -3,32 +3,39 @@ mod macros;
/// Error types and trait implementations. /// Error types and trait implementations.
pub mod error; pub mod error;
/// Types for modelling and interacting with [accounts](https://developer.up.com.au/#accounts). /// Types for modelling and interacting with
/// [accounts](https://developer.up.com.au/#accounts).
pub mod accounts; pub mod accounts;
/// Types for modelling and interacting with [categories](https://developer.up.com.au/#categories). /// Types for modelling and interacting with
/// [categories](https://developer.up.com.au/#categories).
pub mod categories; pub mod categories;
/// Types for modelling and interacting with [tags](https://developer.up.com.au/#tags). /// Types for modelling and interacting with
/// [tags](https://developer.up.com.au/#tags).
pub mod tags; pub mod tags;
/// Types for modelling and interacting with [transactions](https://developer.up.com.au/#transactions). /// Types for modelling and interacting with
/// [transactions](https://developer.up.com.au/#transactions).
pub mod transactions; pub mod transactions;
/// Types for modelling and interacting with [utilities](https://developer.up.com.au/#utility_endpoints). /// Types for modelling and interacting with
/// [utilities](https://developer.up.com.au/#utility_endpoints).
pub mod utilities; pub mod utilities;
/// Types for modelling and interacting with [webhooks](https://developer.up.com.au/#webhooks). /// Types for modelling and interacting with
/// [webhooks](https://developer.up.com.au/#webhooks).
pub mod webhooks; pub mod webhooks;
/// Types which are stardized (and named) across many resources. /// Types which are stardized (and named) across many resources.
pub mod standard; pub mod standard;
static BASE_URL : &str = "https://api.up.com.au/api/v1"; static BASE_URL: &str = "https://api.up.com.au/api/v1";
/// A client for interacting with the Up API. /// A client for interacting with the Up API.
pub struct Client { pub struct Client {
access_token : String, access_token: String,
} }
impl Client { impl Client {
/// Creates an instance of the `Client` from the access token. Visit [this page](https://api.up.com.au/getting_started) to get such a token. /// Creates an instance of the `Client` from the access token. Visit
pub fn new(access_token : String) -> Self { /// [this page](https://api.up.com.au/getting_started) to get such a token.
pub fn new(access_token: String) -> Self {
Client { Client {
access_token access_token
} }

View File

@ -11,11 +11,15 @@ pub enum AccountTypeEnum {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MoneyObject { pub struct MoneyObject {
/// The ISO 4217 currency code. /// The ISO 4217 currency code.
pub currency_code : String, pub currency_code: String,
/// The amount of money, formatted as a string in the relevant currency. For example, for an Australian dollar value of $10.56, this field will be `"10.56"`. The currency symbol is not included in the string /// The amount of money, formatted as a string in the relevant currency.
pub value : String, /// For example, for an Australian dollar value of $10.56, this field will
/// The amount of money in the smallest denomination for the currency, as a 64-bit integer. For example, for an Australian dollar value of $10.56, this field will be `1056`. /// be `"10.56"`. The currency symbol is not included in the string
pub value_in_base_units : i64, pub value: String,
/// The amount of money in the smallest denomination for the currency, as a
/// 64-bit integer. For example, for an Australian dollar value of $10.56,
/// this field will be `1056`.
pub value_in_base_units: i64,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -35,27 +39,33 @@ pub enum TransactionStatusEnum {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct HoldInfoObject { pub struct HoldInfoObject {
/// The amount of this transaction while in the `HELD` status, in Australian dollars. /// The amount of this transaction while in the `HELD` status, in
pub amount : MoneyObject, /// Australian dollars.
/// The foreign currency amount of this transaction while in the `HELD` status. This field will be `null` for domestic transactions. The amount was converted to the AUD amount reflected in the `amount` field. pub amount: MoneyObject,
pub foreign_amount : Option<MoneyObject>, /// The foreign currency amount of this transaction while in the `HELD`
/// status. This field will be `null` for domestic transactions. The amount
/// was converted to the AUD amount reflected in the `amount` field.
pub foreign_amount: Option<MoneyObject>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RoundUpObject { pub struct RoundUpObject {
/// The total amount of this Round Up, including any boosts, represented as a negative value. /// The total amount of this Round Up, including any boosts, represented as
pub amount : MoneyObject, /// a negative value.
/// The portion of the Round Up `amount` owing to boosted Round Ups, represented as a negative value. If no boost was added to the Round Up this field will be `null`. pub amount: MoneyObject,
pub boost_portion : Option<MoneyObject>, /// The portion of the Round Up `amount` owing to boosted Round Ups,
/// represented as a negative value. If no boost was added to the Round Up
/// this field will be `null`.
pub boost_portion: Option<MoneyObject>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CashBackObject { pub struct CashBackObject {
/// A brief description of why this cashback was paid. /// A brief description of why this cashback was paid.
pub description : String, pub description: String,
/// The total amount of cashback paid, represented as a positive value. /// The total amount of cashback paid, represented as a positive value.
pub amount : MoneyObject, pub amount: MoneyObject,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -76,9 +86,9 @@ pub enum CardPurchaseMethodEnum {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CardPurchaseMethodObject { pub struct CardPurchaseMethodObject {
/// The type of card purchase. /// The type of card purchase.
pub method : CardPurchaseMethodEnum, pub method: CardPurchaseMethodEnum,
/// The last four digits of the card used for the purchase, if applicable. /// The last four digits of the card used for the purchase, if applicable.
pub card_number_suffix : Option<String>, pub card_number_suffix: Option<String>,
} }

View File

@ -7,39 +7,39 @@ use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ListTagsResponse { pub struct ListTagsResponse {
/// The list of tags returned in this response. /// The list of tags returned in this response.
pub data : Vec<TagResource>, pub data: Vec<TagResource>,
pub links : ResponseLinks, pub links: ResponseLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TagResource { pub struct TagResource {
/// The type of this resource: `tags` /// The type of this resource: `tags`
pub r#type : String, pub r#type: String,
/// The label of the tag, which also acts as the tags unique identifier. /// The label of the tag, which also acts as the tags unique identifier.
pub id : String, pub id: String,
pub relationships : Relationships, pub relationships: Relationships,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Relationships { pub struct Relationships {
pub transactions : Transactions, pub transactions: Transactions,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Transactions { pub struct Transactions {
pub links : Option<TransactionLinks>, pub links: Option<TransactionLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransactionLinks { pub struct TransactionLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ResponseLinks { pub struct ResponseLinks {
pub prev : Option<String>, pub prev: Option<String>,
pub next : Option<String>, pub next: Option<String>,
} }
// ----------------- Input Objects ----------------- // ----------------- Input Objects -----------------
@ -47,16 +47,16 @@ pub struct ResponseLinks {
#[derive(Default)] #[derive(Default)]
pub struct ListTagsOptions { pub struct ListTagsOptions {
/// The number of records to return in each page. /// The number of records to return in each page.
page_size : Option<u8>, page_size: Option<u8>,
} }
impl ListTagsOptions { impl ListTagsOptions {
/// Sets the page size. /// Sets the page size.
pub fn page_size(&mut self, value : u8) { pub fn page_size(&mut self, value: u8) {
self.page_size = Some(value); self.page_size = Some(value);
} }
fn add_params(&self, url : &mut reqwest::Url) { fn add_params(&self, url: &mut reqwest::Url) {
let mut query = String::new(); let mut query = String::new();
if let Some(value) = &self.page_size { if let Some(value) = &self.page_size {
@ -77,21 +77,30 @@ impl ListTagsOptions {
#[derive(Serialize)] #[derive(Serialize)]
struct TagInputResourceIdentifier { struct TagInputResourceIdentifier {
/// The type of this resource: `tags` /// The type of this resource: `tags`
r#type : String, r#type: String,
/// The label of the tag, which also acts as the tags unique identifier. /// The label of the tag, which also acts as the tags unique identifier.
id : String, id: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct TagRequest { struct TagRequest {
/// The tags to add to or remove from the transaction. /// The tags to add to or remove from the transaction.
data : Vec<TagInputResourceIdentifier> data: Vec<TagInputResourceIdentifier>
} }
impl Client { impl Client {
/// Retrieve a list of all tags currently in use. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered lexicographically. The transactions relationship for each tag exposes a link to get the transactions with the given tag. /// Retrieve a list of all tags currently in use. The returned list is
pub async fn list_tags(&self, options : &ListTagsOptions) -> Result<ListTagsResponse, error::Error> { /// paginated and can be scrolled by following the `next` and `prev` links
let mut url = reqwest::Url::parse(&format!("{}/tags", BASE_URL)).map_err(error::Error::UrlParse)?; /// where present. Results are ordered lexicographically. The transactions
/// relationship for each tag exposes a link to get the transactions with
/// the given tag.
pub async fn list_tags(
&self,
options: &ListTagsOptions,
) -> Result<ListTagsResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/tags", BASE_URL)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -103,35 +112,60 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let tags_response : ListTagsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let tags_response: ListTagsResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(tags_response) Ok(tags_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Associates one or more tags with a specific transaction. No more than 6 tags may be present on any single transaction. Duplicate tags are silently ignored. The associated tags, along with this request URL, are also exposed via the tags relationship on the transaction resource returned from `get_transaction`. /// Associates one or more tags with a specific transaction. No more than 6
pub async fn add_tags(&self, transaction_id : &str, tags : Vec<String>) -> Result<(), error::Error> { /// tags may be present on any single transaction. Duplicate tags are
let url = reqwest::Url::parse(&format!("{}/transactions/{}/relationships/tags", BASE_URL, transaction_id)).map_err(error::Error::UrlParse)?; /// silently ignored. The associated tags, along with this request URL, are
/// also exposed via the tags relationship on the transaction resource
/// returned from `get_transaction`.
pub async fn add_tags(
&self,
transaction_id: &str,
tags: Vec<String>,
) -> Result<(), error::Error> {
let url = reqwest::Url::parse(
&format!("{}/transactions/{}/relationships/tags",
BASE_URL,
transaction_id,
)
).map_err(error::Error::UrlParse)?;
let tags = let tags =
tags tags
.into_iter() .into_iter()
.map(|t| TagInputResourceIdentifier { .map(|t| TagInputResourceIdentifier {
r#type : String::from("tags"), r#type: String::from("tags"),
id : t id: t
}) })
.collect(); .collect();
let body = TagRequest { data : tags }; let body = TagRequest { data: tags };
let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?; let body =
serde_json::to_string(&body)
.map_err(error::Error::Serialize)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.post(url) .post(url)
@ -147,29 +181,48 @@ impl Client {
Ok(()) Ok(())
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Disassociates one or more tags from a specific transaction. Tags that are not associated are silently ignored. The associated tags, along with this request URL, are also exposed via the tags relationship on the transaction resource returned from `get_transaction`. /// Disassociates one or more tags from a specific transaction. Tags that
pub async fn delete_tags(&self, transaction_id : &str, tags : Vec<String>) -> Result<(), error::Error> { /// are not associated are silently ignored. The associated tags, along
let url = reqwest::Url::parse(&format!("{}/transactions/{}/relationships/tags", BASE_URL, transaction_id)).map_err(error::Error::UrlParse)?; /// with this request URL, are also exposed via the tags relationship on
/// the transaction resource returned from `get_transaction`.
pub async fn delete_tags(
&self,
transaction_id: &str,
tags: Vec<String>,
) -> Result<(), error::Error> {
let url = reqwest::Url::parse(
&format!("{}/transactions/{}/relationships/tags",
BASE_URL,
transaction_id,
)
).map_err(error::Error::UrlParse)?;
let tags = let tags =
tags tags
.into_iter() .into_iter()
.map(|t| TagInputResourceIdentifier { .map(|t| TagInputResourceIdentifier {
r#type : String::from("tags"), r#type: String::from("tags"),
id : t id: t
}) })
.collect(); .collect();
let body = TagRequest { data : tags }; let body = TagRequest { data: tags };
let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?; let body =
serde_json::to_string(&body)
.map_err(error::Error::Serialize)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.delete(url) .delete(url)
@ -185,8 +238,13 @@ impl Client {
Ok(()) Ok(())
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }

View File

@ -7,237 +7,278 @@ use serde::Deserialize;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ListTransactionsResponse { pub struct ListTransactionsResponse {
/// The list of transactions returned in this response. /// The list of transactions returned in this response.
pub data : Vec<TransactionResource>, pub data: Vec<TransactionResource>,
pub links : ResponseLinks, pub links: ResponseLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct GetTransactionResponse { pub struct GetTransactionResponse {
/// The transaction returned in this response. /// The transaction returned in this response.
pub data : TransactionResource, pub data: TransactionResource,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransactionResource { pub struct TransactionResource {
/// The type of this resource: `transactions` /// The type of this resource: `transactions`
pub r#type : String, pub r#type: String,
/// The unique identifier for this transaction. /// The unique identifier for this transaction.
pub id : String, pub id: String,
pub attributes : Attributes, pub attributes: Attributes,
pub relationships : Relationships, pub relationships: Relationships,
pub links : Option<TransactionResourceLinks>, pub links: Option<TransactionResourceLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransactionResourceLinks { pub struct TransactionResourceLinks {
/// The canonical link to this resource within the API. /// The canonical link to this resource within the API.
#[serde(rename = "self")] #[serde(rename = "self")]
pub this : String, pub this: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Relationships { pub struct Relationships {
pub account : Account, pub account: Account,
/// If this transaction is a transfer between accounts, this relationship will contain the account the transaction went to/came from. The `amount` field can be used to determine the direction of the transfer. /// If this transaction is a transfer between accounts, this relationship
pub transfer_account : TransferAccount, /// will contain the account the transaction went to/came from. The `amount`
pub category : Category, /// field can be used to determine the direction of the transfer.
pub parent_category : ParentCategory, pub transfer_account: TransferAccount,
pub tags : Tags, pub category: Category,
pub parent_category: ParentCategory,
pub tags: Tags,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Account { pub struct Account {
pub data : AccountData, pub data: AccountData,
pub links : Option<AccountLinks>, pub links: Option<AccountLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct AccountData { pub struct AccountData {
/// The type of this resource: `accounts` /// The type of this resource: `accounts`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct AccountLinks { pub struct AccountLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransferAccount { pub struct TransferAccount {
pub data : Option<AccountData>, pub data: Option<AccountData>,
pub links : Option<AccountLinks>, pub links: Option<AccountLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransferAccountData { pub struct TransferAccountData {
/// The type of this resource: `accounts` /// The type of this resource: `accounts`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransferAccountLinks { pub struct TransferAccountLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Category { pub struct Category {
pub data : Option<CategoryData>, pub data: Option<CategoryData>,
pub links : Option<CategoryLinks>, pub links: Option<CategoryLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CategoryData { pub struct CategoryData {
/// The type of this resource: `categories` /// The type of this resource: `categories`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CategoryLinks { pub struct CategoryLinks {
/// The link to retrieve or modify linkage between this resources and the related resource(s) in this relationship. /// The link to retrieve or modify linkage between this resources and the
/// related resource(s) in this relationship.
#[serde(rename = "self")] #[serde(rename = "self")]
pub this : String, pub this: String,
pub related : Option<String>, pub related: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ParentCategory { pub struct ParentCategory {
pub data : Option<ParentCategoryData>, pub data: Option<ParentCategoryData>,
pub links : Option<ParentCategoryLinks>, pub links: Option<ParentCategoryLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ParentCategoryData { pub struct ParentCategoryData {
/// The type of this resource: `categories` /// The type of this resource: `categories`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ParentCategoryLinks { pub struct ParentCategoryLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Tags { pub struct Tags {
pub data : Vec<TagsData>, pub data: Vec<TagsData>,
pub links : Option<TagsLinks>, pub links: Option<TagsLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TagsData { pub struct TagsData {
/// The type of this resource: `tags` /// The type of this resource: `tags`
pub r#type : String, pub r#type: String,
/// The label of the tag, which also acts as the tags unique identifier. /// The label of the tag, which also acts as the tags unique identifier.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TagsLinks { pub struct TagsLinks {
/// The link to retrieve or modify linkage between this resources and the related resource(s) in this relationship. /// The link to retrieve or modify linkage between this resources and the
/// related resource(s) in this relationship.
#[serde(rename = "self")] #[serde(rename = "self")]
pub this : String, pub this: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Attributes { pub struct Attributes {
/// The current processing status of this transaction, according to whether or not this transaction has settled or is still held. Possible values: `HELD`, `SETTLED` /// The current processing status of this transaction, according to whether
pub status : standard::TransactionStatusEnum, /// or not this transaction has settled or is still held. Possible values:
/// The original, unprocessed text of the transaction. This is often not a perfect indicator of the actual merchant, but it is useful for reconciliation purposes in some cases. /// `HELD`, `SETTLED`
pub raw_text : Option<String>, pub status: standard::TransactionStatusEnum,
/// A short description for this transaction. Usually the merchant name for purchases. /// The original, unprocessed text of the transaction. This is often not a
pub description : String, /// perfect indicator of the actual merchant, but it is useful for
/// Attached message for this transaction, such as a payment message, or a transfer note. /// reconciliation purposes in some cases.
pub message : Option<String>, pub raw_text: Option<String>,
/// Boolean flag set to true on transactions that support the use of categories. /// A short description for this transaction. Usually the merchant name for
pub is_categorizable : bool, /// purchases.
/// If this transaction is currently in the `HELD` status, or was ever in the `HELD` status, the `amount` and `foreignAmount` of the transaction while `HELD`. pub description: String,
pub hold_info : Option<standard::HoldInfoObject>, /// Attached message for this transaction, such as a payment message, or a
/// Details of how this transaction was rounded-up. If no Round Up was applied this field will be `null`. /// transfer note.
pub round_up : Option<standard::RoundUpObject>, pub message: Option<String>,
/// If all or part of this transaction was instantly reimbursed in the form of cashback, details of the reimbursement. /// Boolean flag set to true on transactions that support the use of
pub cashback : Option<standard::CashBackObject>, /// categories.
/// The amount of this transaction in Australian dollars. For transactions that were once `HELD` but are now `SETTLED`, refer to the `holdInfo` field for the original `amount` the transaction was `HELD` at. pub is_categorizable: bool,
pub amount : standard::MoneyObject, /// If this transaction is currently in the `HELD` status, or was ever in
/// The foreign currency amount of this transaction. This field will be `null` for domestic transactions. The amount was converted to the AUD amount reflected in the `amount` of this transaction. Refer to the `holdInfo` field for the original foreignAmount the transaction was `HELD` at. /// the `HELD` status, the `amount` and `foreignAmount` of the transaction
pub foreign_amount : Option<standard::MoneyObject>, /// while `HELD`.
pub hold_info: Option<standard::HoldInfoObject>,
/// Details of how this transaction was rounded-up. If no Round Up was
/// applied this field will be `null`.
pub round_up: Option<standard::RoundUpObject>,
/// If all or part of this transaction was instantly reimbursed in the form
/// of cashback, details of the reimbursement.
pub cashback: Option<standard::CashBackObject>,
/// The amount of this transaction in Australian dollars. For transactions
/// that were once `HELD` but are now `SETTLED`, refer to the `holdInfo`
/// field for the original `amount` the transaction was `HELD` at.
pub amount: standard::MoneyObject,
/// The foreign currency amount of this transaction. This field will be
/// `null` for domestic transactions. The amount was converted to the AUD
/// amount reflected in the `amount` of this transaction. Refer to the
/// `holdInfo` field for the original foreignAmount the transaction was
/// `HELD` at.
pub foreign_amount: Option<standard::MoneyObject>,
/// Information about the card used for this transaction, if applicable. /// Information about the card used for this transaction, if applicable.
pub card_purchase_method : Option<standard::CardPurchaseMethodObject>, pub card_purchase_method: Option<standard::CardPurchaseMethodObject>,
/// The date-time at which this transaction settled. This field will be `null` for transactions that are currently in the `HELD` status. /// The date-time at which this transaction settled. This field will be
pub settled_at : Option<String>, /// `null` for transactions that are currently in the `HELD` status.
pub settled_at: Option<String>,
/// The date-time at which this transaction was first encountered. /// The date-time at which this transaction was first encountered.
pub created_at : String, pub created_at: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ResponseLinks { pub struct ResponseLinks {
/// The link to the previous page in the results. If this value is null there is no previous page. /// The link to the previous page in the results. If this value is null
pub prev : Option<String>, /// there is no previous page.
/// The link to the next page in the results. If this value is null there is no next page. pub prev: Option<String>,
pub next : Option<String>, /// The link to the next page in the results. If this value is null there is
/// no next page.
pub next: Option<String>,
} }
// ----------------- Input Objects ----------------- // ----------------- Input Objects -----------------
#[derive(Default)] pub struct ListTransactionsOptions<Tz: chrono::TimeZone> {
pub struct ListTransactionsOptions {
/// The number of records to return in each page. /// The number of records to return in each page.
page_size : Option<u8>, page_size: Option<u8>,
/// The transaction status for which to return records. This can be used to filter `HELD` transactions from those that are `SETTLED`. /// The transaction status for which to return records. This can be used to
filter_status : Option<String>, /// filter `HELD` transactions from those that are `SETTLED`.
/// The start date-time from which to return records, formatted according to rfc-3339. Not to be used for pagination purposes. filter_status: Option<String>,
filter_since : Option<String>, /// The start date-time from which to return records, formatted according to
/// The end date-time up to which to return records, formatted according to rfc-3339. Not to be used for pagination purposes. /// rfc-3339. Not to be used for pagination purposes.
filter_until : Option<String>, filter_since: Option<chrono::DateTime<Tz>>,
/// The category identifier for which to filter transactions. Both parent and child categories can be filtered through this parameter. /// The end date-time up to which to return records, formatted according to
filter_category : Option<String>, /// rfc-3339. Not to be used for pagination purposes.
/// A transaction tag to filter for which to return records. If the tag does not exist, zero records are returned and a success response is given. filter_until: Option<chrono::DateTime<Tz>>,
filter_tag : Option<String>, /// The category identifier for which to filter transactions. Both parent
/// and child categories can be filtered through this parameter.
filter_category: Option<String>,
/// A transaction tag to filter for which to return records. If the tag does
/// not exist, zero records are returned and a success response is given.
filter_tag: Option<String>,
} }
impl ListTransactionsOptions { impl<Tz: chrono::TimeZone> Default for ListTransactionsOptions<Tz> {
fn default() -> Self {
Self {
page_size: None,
filter_status: None,
filter_since: None,
filter_until: None,
filter_category: None,
filter_tag: None,
}
}
}
impl<Tz: chrono::TimeZone> ListTransactionsOptions<Tz> {
/// Sets the page size. /// Sets the page size.
pub fn page_size(&mut self, value : u8) { pub fn page_size(&mut self, value: u8) {
self.page_size = Some(value); self.page_size = Some(value);
} }
/// Sets the status filter value. /// Sets the status filter value.
pub fn filter_status(&mut self, value : String) { pub fn filter_status(&mut self, value: String) {
self.filter_status = Some(value); self.filter_status = Some(value);
} }
/// Sets the since filter value. /// Sets the since filter value.
pub fn filter_since(&mut self, value : String) { pub fn filter_since(&mut self, value: chrono::DateTime<Tz>) {
self.filter_since = Some(value); self.filter_since = Some(value);
} }
/// Sets the until filter value. /// Sets the until filter value.
pub fn filter_until (&mut self, value : String) { pub fn filter_until (&mut self, value: chrono::DateTime<Tz>) {
self.filter_until = Some(value); self.filter_until = Some(value);
} }
/// Sets the category filter value. /// Sets the category filter value.
pub fn filter_category(&mut self, value : String) { pub fn filter_category(&mut self, value: String) {
self.filter_category = Some(value); self.filter_category = Some(value);
} }
/// Sets the tag filter value. /// Sets the tag filter value.
pub fn filter_tag (&mut self, value : String) { pub fn filter_tag (&mut self, value: String) {
self.filter_tag = Some(value); self.filter_tag = Some(value);
} }
fn add_params(&self, url : &mut reqwest::Url) { fn add_params(&self, url: &mut reqwest::Url) {
let mut query = String::new(); let mut query = String::new();
if let Some(value) = &self.page_size { if let Some(value) = &self.page_size {
@ -251,35 +292,45 @@ impl ListTransactionsOptions {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[status]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[status]={}", urlencoding::encode(value))
);
} }
if let Some(value) = &self.filter_since { if let Some(value) = &self.filter_since {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[since]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[since]={}", urlencoding::encode(&value.to_rfc3339()))
);
} }
if let Some(value) = &self.filter_until { if let Some(value) = &self.filter_until {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[until]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[until]={}", urlencoding::encode(&value.to_rfc3339()))
);
} }
if let Some(value) = &self.filter_category { if let Some(value) = &self.filter_category {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[category]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[category]={}", urlencoding::encode(value))
);
} }
if let Some(value) = &self.filter_tag { if let Some(value) = &self.filter_tag {
if !query.is_empty() { if !query.is_empty() {
query.push('&'); query.push('&');
} }
query.push_str(&format!("filter[tag]={}", urlencoding::encode(value))); query.push_str(
&format!("filter[tag]={}", urlencoding::encode(value))
);
} }
if !query.is_empty() { if !query.is_empty() {
@ -289,9 +340,20 @@ impl ListTransactionsOptions {
} }
impl Client { impl Client {
/// Retrieve a list of all transactions across all accounts for the currently authenticated user. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. To narrow the results to a specific date range pass one or both of `filter[since]` and `filter[until]` in the query string. These filter parameters should not be used for pagination. Results are ordered newest first to oldest last. /// Retrieve a list of all transactions across all accounts for the
pub async fn list_transactions(&self, options : &ListTransactionsOptions) -> Result<ListTransactionsResponse, error::Error> { /// currently authenticated user. The returned list is paginated and can be
let mut url = reqwest::Url::parse(&format!("{}/transactions", BASE_URL)).map_err(error::Error::UrlParse)?; /// scrolled by following the `next` and `prev` links where present. To
/// narrow the results to a specific date range pass one or both of
/// `filter[since]` and `filter[until]` in the query string. These filter
/// parameters should not be used for pagination. Results are ordered newest
/// first to oldest last.
pub async fn list_transactions<Tz: chrono::TimeZone>(
&self,
options: &ListTransactionsOptions<Tz>,
) -> Result<ListTransactionsResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/transactions", BASE_URL)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -303,14 +365,24 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let transaction_response : ListTransactionsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let transaction_response: ListTransactionsResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(transaction_response) Ok(transaction_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
@ -318,14 +390,20 @@ impl Client {
} }
/// Retrieve a specific transaction by providing its unique identifier. /// Retrieve a specific transaction by providing its unique identifier.
pub async fn get_transaction(&self, id : &String) -> Result<GetTransactionResponse, error::Error> { pub async fn get_transaction(
// This assertion is because without an ID the request is thought to be a request for &self,
// many transactions, and therefore the error messages are very unclear. id: &String,
) -> Result<GetTransactionResponse, error::Error> {
// This assertion is because without an ID the request is thought to be
// a request for many transactions, and therefore the error messages
// are very unclear.
if id.is_empty() { if id.is_empty() {
panic!("The provided transaction ID must not be empty."); panic!("The provided transaction ID must not be empty.");
} }
let url = reqwest::Url::parse(&format!("{}/transactions/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; let url = reqwest::Url::parse(
&format!("{}/transactions/{}", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(url) .get(url)
@ -337,22 +415,35 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let transaction_response : GetTransactionResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let transaction_response: GetTransactionResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Ok(transaction_response) Ok(transaction_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Retrieve a list of all transactions for a specific account. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. To narrow the results to a specific date range pass one or both of `filter[since]` and `filter[until]` in the query string. These filter parameters should not be used for pagination. Results are ordered newest first to oldest last. /// Retrieve a list of all transactions for a specific account. The returned
pub async fn list_transactions_by_account(&self, account_id : &String, options : &ListTransactionsOptions) -> Result<ListTransactionsResponse, error::Error> { /// list is paginated and can be scrolled by following the `next` and `prev`
let mut url = reqwest::Url::parse(&format!("{}/accounts/{}/transactions", BASE_URL, account_id)).map_err(error::Error::UrlParse)?; /// links where present. To narrow the results to a specific date range pass
/// one or both of `filter[since]` and `filter[until]` in the query string.
/// These filter parameters should not be used for pagination. Results are
/// ordered newest first to oldest last.
pub async fn list_transactions_by_account<Tz: chrono::TimeZone>(
&self,
account_id: &String,
options: &ListTransactionsOptions<Tz>,
) -> Result<ListTransactionsResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/accounts/{}/transactions", BASE_URL, account_id)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -365,13 +456,15 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let transaction_response : ListTransactionsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let transaction_response: ListTransactionsResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Ok(transaction_response) Ok(transaction_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body = res.text().await.map_err(error::Error::BodyRead)?;
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let error: error::ErrorResponse =
serde_json::from_str(&body).map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }

View File

@ -6,22 +6,25 @@ use serde::Deserialize;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct PingResponse { pub struct PingResponse {
pub meta : Meta, pub meta: Meta,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Meta { pub struct Meta {
/// The unique identifier of the authenticated customer. /// The unique identifier of the authenticated customer.
pub id : String, pub id: String,
#[serde(rename = "statusEmoji")] #[serde(rename = "statusEmoji")]
/// A cute emoji that represents the response status. /// A cute emoji that represents the response status.
pub status_emoji : String, pub status_emoji: String,
} }
impl Client { impl Client {
/// Make a basic ping request to the API. This is useful to verify that authentication is functioning correctly. /// Make a basic ping request to the API. This is useful to verify that
/// authentication is functioning correctly.
pub async fn ping(&self) -> Result<PingResponse, error::Error> { pub async fn ping(&self) -> Result<PingResponse, error::Error> {
let url = reqwest::Url::parse(&format!("{}/util/ping", BASE_URL)).map_err(error::Error::UrlParse)?; let url = reqwest::Url::parse(
&format!("{}/util/ping", BASE_URL)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(url) .get(url)
@ -32,15 +35,25 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
res.text()
.await
.map_err(error::Error::BodyRead)?;
println!("{}", body); println!("{}", body);
let ping_response : PingResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; let ping_response: PingResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(ping_response) Ok(ping_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }

View File

@ -7,220 +7,235 @@ use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ListWebhooksResponse { pub struct ListWebhooksResponse {
/// The list of webhooks returned in this response. /// The list of webhooks returned in this response.
pub data : Vec<WebhookResource>, pub data: Vec<WebhookResource>,
pub links : ResponseLinks, pub links: ResponseLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct GetWebhookResponse { pub struct GetWebhookResponse {
/// The webhook returned in the response. /// The webhook returned in the response.
pub data : WebhookResource, pub data: WebhookResource,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CreateWebhookResponse { pub struct CreateWebhookResponse {
/// The webhook that was created. /// The webhook that was created.
pub data : WebhookResource, pub data: WebhookResource,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookResource { pub struct WebhookResource {
/// The type of this resource: `webhooks` /// The type of this resource: `webhooks`
pub r#type : String, pub r#type: String,
/// The unique identifier for this webhook. /// The unique identifier for this webhook.
pub id : String, pub id: String,
pub attributes : Attributes, pub attributes: Attributes,
pub relationships : Relationships, pub relationships: Relationships,
pub links : WebhookResourceLinks, pub links: WebhookResourceLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Attributes { pub struct Attributes {
/// The URL that this webhook is configured to `POST` events to. /// The URL that this webhook is configured to `POST` events to.
pub url : String, pub url: String,
/// An optional description that was provided at the time the webhook was created. /// An optional description that was provided at the time the webhook was
pub description : Option<String>, /// created.
/// A shared secret key used to sign all webhook events sent to the configured webhook URL. This field is returned only once, upon the initial creation of the webhook. If lost, create a new webhook and delete this webhook. pub description: Option<String>,
/// The webhook URL receives a request with a `X-Up-Authenticity-Signature` header, which is the SHA-256 HMAC of the entire raw request body signed using this `secretKey`. It is advised to compute and check this signature to verify the authenticity of requests sent to the webhook URL. See Handling webhook events for full details. /// A shared secret key used to sign all webhook events sent to the
pub secret_key : Option<String>, /// configured webhook URL. This field is returned only once, upon the
/// initial creation of the webhook. If lost, create a new webhook and
/// delete this webhook.
///
/// The webhook URL receives a request with a `X-Up-Authenticity-Signature`
/// header, which is the SHA-256 HMAC of the entire raw request body signed
/// using this `secretKey`. It is advised to compute and check this
/// signature to verify the authenticity of requests sent to the webhook
/// URL. See Handling webhook events for full details.
pub secret_key: Option<String>,
/// The date-time at which this webhook was created. /// The date-time at which this webhook was created.
pub created_at : String, pub created_at: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Relationships { pub struct Relationships {
pub logs : Logs, pub logs: Logs,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Logs { pub struct Logs {
pub links : Option<LogsLinks>, pub links: Option<LogsLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct LogsLinks { pub struct LogsLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookResourceLinks { pub struct WebhookResourceLinks {
/// The canonical link to this resource within the API. /// The canonical link to this resource within the API.
#[serde(rename = "self")] #[serde(rename = "self")]
pub this : String, pub this: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ResponseLinks { pub struct ResponseLinks {
/// The link to the previous page in the results. If this value is `None` there is no previous page. /// The link to the previous page in the results. If this value is `None`
pub prev : Option<String>, /// there is no previous page.
/// The link to the next page in the results. If this value is `None` there is no next page. pub prev: Option<String>,
pub next : Option<String>, /// The link to the next page in the results. If this value is `None` there
/// is no next page.
pub next: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct PingWebhookResponse { pub struct PingWebhookResponse {
/// The webhook event data sent to the subscribed webhook. /// The webhook event data sent to the subscribed webhook.
pub data : WebhookEventResource, pub data: WebhookEventResource,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookEventResource { pub struct WebhookEventResource {
/// The type of this resource: `webhook-events` /// The type of this resource: `webhook-events`
pub r#type : String, pub r#type: String,
/// The unique identifier for this event. This will remain constant across delivery retries. /// The unique identifier for this event. This will remain constant across
pub id : String, /// delivery retries.
pub attributes : EventAttributes, pub id: String,
pub relationships : EventRelationships, pub attributes: EventAttributes,
pub relationships: EventRelationships,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct EventRelationships { pub struct EventRelationships {
pub webhook : Webhook, pub webhook: Webhook,
pub transaction : Option<Transaction>, pub transaction: Option<Transaction>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Transaction { pub struct Transaction {
pub data : TransactionData, pub data: TransactionData,
pub links : Option<TransactionLinks>, pub links: Option<TransactionLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Webhook { pub struct Webhook {
pub data : WebhookData, pub data: WebhookData,
pub links : Option<WebhookLinks>, pub links: Option<WebhookLinks>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookData { pub struct WebhookData {
/// The type of this resource: `webhooks` /// The type of this resource: `webhooks`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookLinks { pub struct WebhookLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransactionData { pub struct TransactionData {
/// The type of this resource: `transactions` /// The type of this resource: `transactions`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TransactionLinks { pub struct TransactionLinks {
/// The link to retrieve the related resource(s) in this relationship. /// The link to retrieve the related resource(s) in this relationship.
pub related : String, pub related: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EventAttributes { pub struct EventAttributes {
/// The type of this event. This can be used to determine what action to take in response to the event. /// The type of this event. This can be used to determine what action to
pub event_type : standard::WebhookEventTypeEnum, /// take in response to the event.
pub event_type: standard::WebhookEventTypeEnum,
/// The date-time at which this event was generated. /// The date-time at which this event was generated.
pub created_at : String, pub created_at: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ListWebhookLogsResponse { pub struct ListWebhookLogsResponse {
/// The list of delivery logs returned in this response. /// The list of delivery logs returned in this response.
pub data : Vec<WebhookDeliveryLogResource>, pub data: Vec<WebhookDeliveryLogResource>,
pub links : LogsResponseLinks, pub links: LogsResponseLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookDeliveryLogResource { pub struct WebhookDeliveryLogResource {
/// The type of this resource: `webhook-delivery-logs` /// The type of this resource: `webhook-delivery-logs`
pub r#type : String, pub r#type: String,
/// The unique identifier for this log entry. /// The unique identifier for this log entry.
pub id : String, pub id: String,
pub attributes : DeliveryLogAttributes, pub attributes: DeliveryLogAttributes,
pub relationships : DeliveryLogRelationships, pub relationships: DeliveryLogRelationships,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DeliveryLogRelationships { pub struct DeliveryLogRelationships {
pub webhook_event : WebhookEvent, pub webhook_event: WebhookEvent,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookEvent { pub struct WebhookEvent {
pub data : WebhookEventData pub data: WebhookEventData
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct WebhookEventData { pub struct WebhookEventData {
/// The type of this resource: `webhook-events` /// The type of this resource: `webhook-events`
pub r#type : String, pub r#type: String,
/// The unique identifier of the resource within its type. /// The unique identifier of the resource within its type.
pub id : String, pub id: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DeliveryLogAttributes { pub struct DeliveryLogAttributes {
/// Information about the request that was sent to the webhook URL. /// Information about the request that was sent to the webhook URL.
pub request : Request, pub request: Request,
/// Information about the response that was received from the webhook URL. /// Information about the response that was received from the webhook URL.
pub response : Option<Response>, pub response: Option<Response>,
/// The success or failure status of this delivery attempt. /// The success or failure status of this delivery attempt.
pub delivery_status : standard::WebhookDeliveryStatusEnum, pub delivery_status: standard::WebhookDeliveryStatusEnum,
/// The date-time at which this log entry was created. /// The date-time at which this log entry was created.
pub created_at : String, pub created_at: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Request { pub struct Request {
/// The payload that was sent in the request body. /// The payload that was sent in the request body.
pub body : String, pub body: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Response { pub struct Response {
/// The HTTP status code received in the response. /// The HTTP status code received in the response.
pub status_code : i64, pub status_code: i64,
/// The payload that was received in the response body. /// The payload that was received in the response body.
pub body : String, pub body: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct LogsResponseLinks { pub struct LogsResponseLinks {
/// The link to the previous page in the results. If this value is `None` there is no previous page. /// The link to the previous page in the results. If this value is `None`
pub prev : Option<String>, /// there is no previous page.
/// The link to the next page in the results. If this value is `None` there is no next page. pub prev: Option<String>,
pub next : Option<String>, /// The link to the next page in the results. If this value is `None` there
/// is no next page.
pub next: Option<String>,
} }
@ -229,16 +244,16 @@ pub struct LogsResponseLinks {
#[derive(Default)] #[derive(Default)]
pub struct ListWebhooksOptions { pub struct ListWebhooksOptions {
/// The number of records to return in each page. /// The number of records to return in each page.
page_size : Option<u8>, page_size: Option<u8>,
} }
impl ListWebhooksOptions { impl ListWebhooksOptions {
/// Sets the page size. /// Sets the page size.
pub fn page_size(&mut self, value : u8) { pub fn page_size(&mut self, value: u8) {
self.page_size = Some(value); self.page_size = Some(value);
} }
fn add_params(&self, url : &mut reqwest::Url) { fn add_params(&self, url: &mut reqwest::Url) {
let mut query = String::new(); let mut query = String::new();
if let Some(value) = &self.page_size { if let Some(value) = &self.page_size {
@ -257,16 +272,16 @@ impl ListWebhooksOptions {
#[derive(Default)] #[derive(Default)]
pub struct ListWebhookLogsOptions { pub struct ListWebhookLogsOptions {
/// The number of records to return in each page. /// The number of records to return in each page.
page_size : Option<u8>, page_size: Option<u8>,
} }
impl ListWebhookLogsOptions { impl ListWebhookLogsOptions {
/// Sets the page size. /// Sets the page size.
pub fn page_size(&mut self, value : u8) { pub fn page_size(&mut self, value: u8) {
self.page_size = Some(value); self.page_size = Some(value);
} }
fn add_params(&self, url : &mut reqwest::Url) { fn add_params(&self, url: &mut reqwest::Url) {
let mut query = String::new(); let mut query = String::new();
if let Some(value) = &self.page_size { if let Some(value) = &self.page_size {
@ -287,26 +302,34 @@ impl ListWebhookLogsOptions {
#[derive(Serialize)] #[derive(Serialize)]
pub struct CreateWebhookRequest { pub struct CreateWebhookRequest {
/// The webhook resource to create. /// The webhook resource to create.
pub data : WebhookInputResource, pub data: WebhookInputResource,
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct WebhookInputResource { pub struct WebhookInputResource {
pub attributes : InputAttributes, pub attributes: InputAttributes,
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct InputAttributes { pub struct InputAttributes {
/// The URL that this webhook should post events to. This must be a valid HTTP or HTTPS URL that does not exceed 300 characters in length. /// The URL that this webhook should post events to. This must be a valid
pub url : String, /// HTTP or HTTPS URL that does not exceed 300 characters in length.
pub url: String,
/// An optional description for this webhook, up to 64 characters in length. /// An optional description for this webhook, up to 64 characters in length.
pub description : Option<String>, pub description: Option<String>,
} }
impl Client { impl Client {
/// Retrieve a list of configured webhooks. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered oldest first to newest last. /// Retrieve a list of configured webhooks. The returned list is paginated
pub async fn list_webhooks(&self, options : &ListWebhooksOptions) -> Result<ListWebhooksResponse, error::Error> { /// and can be scrolled by following the `next` and `prev` links where
let mut url = reqwest::Url::parse(&format!("{}/webhooks", BASE_URL)).map_err(error::Error::UrlParse)?; /// present. Results are ordered oldest first to newest last.
pub async fn list_webhooks(
&self,
options: &ListWebhooksOptions,
) -> Result<ListWebhooksResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/webhooks", BASE_URL)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -318,14 +341,24 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let webhook_response : ListWebhooksResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let webhook_response: ListWebhooksResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(webhook_response) Ok(webhook_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
@ -333,14 +366,20 @@ impl Client {
} }
/// Retrieve a specific webhook by providing its unique identifier. /// Retrieve a specific webhook by providing its unique identifier.
pub async fn get_webhook(&self, id : &str) -> Result<GetWebhookResponse, error::Error> { pub async fn get_webhook(
// This assertion is because without an ID the request is thought to be a request for &self,
// many webhooks, and therefore the error messages are very unclear. id: &str,
) -> Result<GetWebhookResponse, error::Error> {
// This assertion is because without an ID the request is thought to be
// a request for many webhooks, and therefore the error messages are
// very unclear.
if id.is_empty() { if id.is_empty() {
panic!("The provided webhook ID must not be empty."); panic!("The provided webhook ID must not be empty.");
} }
let url = reqwest::Url::parse(&format!("{}/webhooks/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; let url = reqwest::Url::parse(
&format!("{}/webhooks/{}", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.get(url) .get(url)
@ -351,34 +390,70 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let webhook_response : GetWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let webhook_response: GetWebhookResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(webhook_response) Ok(webhook_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Create a new webhook with a given URL. The URL will receive webhook events as JSON-encoded `POST` requests. The URL must respond with a HTTP `200` status on success. /// Create a new webhook with a given URL. The URL will receive webhook
/// There is currently a limit of 10 webhooks at any given time. Once this limit is reached, existing webhooks will need to be deleted before new webhooks can be created. /// events as JSON-encoded `POST` requests. The URL must respond with a
/// Event delivery is retried with exponential backoff if the URL is unreachable or it does not respond with a `200` status. The response includes a `secretKey` attribute, which is used to sign requests sent to the webhook URL. It will not be returned from any other endpoints within the Up API. If the `secretKey` is lost, simply create a new webhook with the same URL, capture its `secretKey` and then delete the original webhook. See Handling webhook events for details on how to process webhook events. /// HTTP `200` status on success.
/// It is probably a good idea to test the webhook by sending it a `PING` event after creating it. ///
pub async fn create_webhook(&self, webhook_url : &str, description : Option<String>) -> Result<CreateWebhookResponse, error::Error> { /// There is currently a limit of 10 webhooks at any given time. Once this
let url = reqwest::Url::parse(&format!("{}/webhooks", BASE_URL)).map_err(error::Error::UrlParse)?; /// limit is reached, existing webhooks will need to be deleted before new
/// webhooks can be created.
///
/// Event delivery is retried with exponential backoff if the URL is
/// unreachable or it does not respond with a `200` status. The response
/// includes a `secretKey` attribute, which is used to sign requests sent
/// to the webhook URL. It will not be returned from any other endpoints
/// within the Up API. If the `secretKey` is lost, simply create a new
/// webhook with the same URL, capture its `secretKey` and then delete the
/// original webhook. See Handling webhook events for details on how to
/// process webhook events.
///
/// It is probably a good idea to test the webhook by sending it a `PING`
/// event after creating it.
pub async fn create_webhook(
&self,
webhook_url: &str,
description: Option<String>,
) -> Result<CreateWebhookResponse, error::Error> {
let url = reqwest::Url::parse(
&format!("{}/webhooks", BASE_URL)
).map_err(error::Error::UrlParse)?;
let body = CreateWebhookRequest { let body = CreateWebhookRequest {
data : WebhookInputResource { data: WebhookInputResource {
attributes : InputAttributes { url : String::from(webhook_url), description } attributes: InputAttributes {
url: String::from(webhook_url),
description,
}
} }
}; };
let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?; let body =
serde_json::to_string(&body)
.map_err(error::Error::Serialize)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.post(url) .post(url)
@ -391,23 +466,36 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::CREATED => { reqwest::StatusCode::CREATED => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let webhook_response : CreateWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let webhook_response: CreateWebhookResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(webhook_response) Ok(webhook_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Delete a specific webhook by providing its unique identifier. Once deleted, webhook events will no longer be sent to the configured URL. /// Delete a specific webhook by providing its unique identifier. Once
pub async fn delete_webhook(&self, id : &str) -> Result<(), error::Error> { /// deleted, webhook events will no longer be sent to the configured URL.
let url = reqwest::Url::parse(&format!("{}/webhooks/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?; pub async fn delete_webhook(&self, id: &str) -> Result<(), error::Error> {
let url = reqwest::Url::parse(
&format!("{}/webhooks/{}", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.delete(url) .delete(url)
@ -421,17 +509,30 @@ impl Client {
Ok(()) Ok(())
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Send a `PING` event to a webhook by providing its unique identifier. This is useful for testing and debugging purposes. The event is delivered asynchronously and its data is returned in the response to this request. /// Send a `PING` event to a webhook by providing its unique identifier.
pub async fn ping_webhook(&self, id : &str) -> Result<PingWebhookResponse, error::Error> { /// This is useful for testing and debugging purposes. The event is
let url = reqwest::Url::parse(&format!("{}/webhooks/{}/ping", BASE_URL, id)).map_err(error::Error::UrlParse)?; /// delivered asynchronously and its data is returned in the response to
/// this request.
pub async fn ping_webhook(
&self,
id: &str,
) -> Result<PingWebhookResponse, error::Error> {
let url = reqwest::Url::parse(
&format!("{}/webhooks/{}/ping", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
let res = reqwest::Client::new() let res = reqwest::Client::new()
.post(url) .post(url)
@ -444,23 +545,43 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::CREATED => { reqwest::StatusCode::CREATED => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let webhook_response : PingWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let webhook_response: PingWebhookResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(webhook_response) Ok(webhook_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }
} }
} }
/// Retrieve a list of delivery logs for a webhook by providing its unique identifier. This is useful for analysis and debugging purposes. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered newest first to oldest last. Logs may be automatically purged after a period of time. /// Retrieve a list of delivery logs for a webhook by providing its unique
pub async fn list_webhook_logs(&self, id : &str, options : &ListWebhookLogsOptions) -> Result<ListWebhookLogsResponse, error::Error> { /// identifier. This is useful for analysis and debugging purposes. The
let mut url = reqwest::Url::parse(&format!("{}/webhooks/{}/logs", BASE_URL, id)).map_err(error::Error::UrlParse)?; /// returned list is paginated and can be scrolled by following the `next`
/// and `prev` links where present. Results are ordered newest first to
/// oldest last. Logs may be automatically purged after a period of time.
pub async fn list_webhook_logs(
&self,
id: &str,
options: &ListWebhookLogsOptions,
) -> Result<ListWebhookLogsResponse, error::Error> {
let mut url = reqwest::Url::parse(
&format!("{}/webhooks/{}/logs", BASE_URL, id)
).map_err(error::Error::UrlParse)?;
options.add_params(&mut url); options.add_params(&mut url);
let res = reqwest::Client::new() let res = reqwest::Client::new()
@ -472,14 +593,24 @@ impl Client {
match res.status() { match res.status() {
reqwest::StatusCode::OK => { reqwest::StatusCode::OK => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let webhook_response : ListWebhookLogsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let webhook_response: ListWebhookLogsResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Ok(webhook_response) Ok(webhook_response)
}, },
_ => { _ => {
let body = res.text().await.map_err(error::Error::BodyRead)?; let body =
let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?; res.text()
.await
.map_err(error::Error::BodyRead)?;
let error: error::ErrorResponse =
serde_json::from_str(&body)
.map_err(error::Error::Json)?;
Err(error::Error::Api(error)) Err(error::Error::Api(error))
} }