use crate::config::{err, ok, Client, Response};
use crate::error::Error;
use crate::resources::ApiVersion;
use serde::de::DeserializeOwned;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Default)]
pub struct AppInfo {
pub name: String,
pub url: Option<String>,
pub version: Option<String>,
}
#[derive(Clone, Default)]
pub struct Headers {
pub client_id: Option<String>,
pub stripe_version: Option<ApiVersion>,
pub stripe_account: Option<String>,
pub user_agent: Option<String>,
}
pub trait Object {
type Id;
fn id(&self) -> Self::Id;
fn object(&self) -> &'static str;
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Deleted<T> {
pub id: T,
pub deleted: bool,
}
#[doc(hidden)]
#[derive(Serialize)]
pub struct Expand<'a> {
#[serde(skip_serializing_if = "Expand::is_empty")]
pub expand: &'a [&'a str],
}
impl Expand<'_> {
pub(crate) fn is_empty(expand: &[&str]) -> bool {
expand.is_empty()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)]
pub enum Expandable<T: Object> {
Id(T::Id),
Object(Box<T>),
}
impl<T> Expandable<T>
where
T: Object,
T::Id: Clone,
{
pub fn id(&self) -> T::Id {
match self {
Expandable::Id(id) => id.clone(),
Expandable::Object(obj) => obj.id(),
}
}
}
impl<T: Object> Expandable<T> {
pub fn is_object(&self) -> bool {
match self {
Expandable::Id(_) => false,
Expandable::Object(_) => true,
}
}
pub fn as_object(&self) -> Option<&T> {
match self {
Expandable::Id(_) => None,
Expandable::Object(obj) => Some(&obj),
}
}
#[deprecated(
note = "Renamed `into_object` per rust api design guidelines (may be removed in v0.12)"
)]
#[allow(clippy::wrong_self_convention)]
pub fn to_object(self) -> Option<T> {
match self {
Expandable::Id(_) => None,
Expandable::Object(obj) => Some(*obj),
}
}
pub fn into_object(self) -> Option<T> {
match self {
Expandable::Id(_) => None,
Expandable::Object(obj) => Some(*obj),
}
}
}
pub trait Paginate {
type Cursor: AsCursor;
fn cursor(&self) -> Self::Cursor;
}
pub trait AsCursor: AsRef<str> {}
impl<'a> AsCursor for &'a str {}
impl AsCursor for String {}
impl<T> Paginate for T
where
T: Object,
T::Id: AsCursor,
{
type Cursor = T::Id;
fn cursor(&self) -> Self::Cursor {
self.id()
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct List<T> {
pub data: Vec<T>,
pub has_more: bool,
pub total_count: Option<u64>,
pub url: String,
}
impl<T> Default for List<T> {
fn default() -> Self {
List { data: Vec::new(), has_more: false, total_count: None, url: String::new() }
}
}
impl<T: Clone> Clone for List<T> {
fn clone(&self) -> Self {
List {
data: self.data.clone(),
has_more: self.has_more,
total_count: self.total_count,
url: self.url.clone(),
}
}
}
impl<T: DeserializeOwned + Send + 'static> List<T> {
pub fn get_next(client: &Client, url: &str, last_id: &str) -> Response<List<T>> {
if url.starts_with("/v1/") {
let mut url = url.trim_start_matches("/v1/").to_string();
if url.contains('?') {
url.push_str(&format!("&starting_after={}", last_id));
} else {
url.push_str(&format!("?starting_after={}", last_id));
}
client.get(&url)
} else {
err(Error::Unsupported("URL for fetching additional data uses different API version"))
}
}
}
impl<T: Paginate + DeserializeOwned + Send + 'static> List<T> {
#[cfg(all(feature = "blocking", not(feature = "async")))]
pub fn get_all(self, client: &Client) -> Response<Vec<T>> {
let mut data = Vec::new();
let mut next = self;
loop {
if next.has_more {
let resp = next.next(&client)?;
data.extend(next.data);
next = resp;
} else {
data.extend(next.data);
break;
}
}
Ok(data)
}
pub fn next(&self, client: &Client) -> Response<List<T>> {
if let Some(last_id) = self.data.last().map(|d| d.cursor()) {
List::get_next(client, &self.url, last_id.as_ref())
} else {
ok(List {
data: Vec::new(),
has_more: false,
total_count: self.total_count,
url: self.url.clone(),
})
}
}
}
pub type Metadata = HashMap<String, String>;
pub type Timestamp = i64;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub struct RangeBounds<T> {
pub gt: Option<T>,
pub gte: Option<T>,
pub lt: Option<T>,
pub lte: Option<T>,
}
impl<T> Default for RangeBounds<T> {
fn default() -> Self {
RangeBounds { gt: None, gte: None, lt: None, lte: None }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RangeQuery<T> {
Exact(T),
Bounds(RangeBounds<T>),
}
impl<T> RangeQuery<T> {
pub fn eq(value: T) -> RangeQuery<T> {
RangeQuery::Exact(value)
}
pub fn gt(value: T) -> RangeQuery<T> {
let mut bounds = RangeBounds::default();
bounds.gt = Some(value);
RangeQuery::Bounds(bounds)
}
pub fn gte(value: T) -> RangeQuery<T> {
let mut bounds = RangeBounds::default();
bounds.gte = Some(value);
RangeQuery::Bounds(bounds)
}
pub fn lt(value: T) -> RangeQuery<T> {
let mut bounds = RangeBounds::default();
bounds.lt = Some(value);
RangeQuery::Bounds(bounds)
}
pub fn lte(value: T) -> RangeQuery<T> {
let mut bounds = RangeBounds::default();
bounds.lte = Some(value);
RangeQuery::Bounds(bounds)
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum IdOrCreate<'a, T> {
Id(&'a str),
Create(&'a T),
}
pub fn to_snakecase(camel: &str) -> String {
let mut i = 0;
let mut snake = String::new();
let mut chars = camel.chars().peekable();
while let Some(ch) = chars.next() {
if ch.is_uppercase() {
if i > 0 && !chars.peek().unwrap_or(&'A').is_uppercase() {
snake.push('_');
}
snake.push(ch.to_lowercase().next().unwrap_or(ch));
} else {
snake.push(ch);
}
i += 1;
}
snake
}
#[cfg(test)]
mod tests {
#[test]
fn to_snakecase() {
use super::to_snakecase;
assert_eq!(to_snakecase("snake_case").as_str(), "snake_case");
assert_eq!(to_snakecase("CamelCase").as_str(), "camel_case");
assert_eq!(to_snakecase("XMLHttpRequest").as_str(), "xml_http_request");
assert_eq!(to_snakecase("UPPER").as_str(), "upper");
assert_eq!(to_snakecase("lower").as_str(), "lower");
}
}