use crate::errors::{api_error, Error, Kind, Result};
use crate::models::{FeedItem, InputItem};
use crate::{api_token, env_or_default_url, normalize_item_time, X_AUTH_TOKEN_HEADER};
use serde::{Deserialize, Serialize};
use serde_json::from_str as json_from_str;
use std::time::Duration;
use tokio::time::sleep;
pub trait YupdatesV0 {
fn new_items(&self, items: &[InputItem]) -> Result<NewInputItemsResponse>;
fn new_items_all(&self, items: &[InputItem], sleep_ms: u64) -> Result<String>;
fn ping(&self) -> Result<PingResponse>;
fn ping_bool(&self) -> bool;
fn read_items<S>(&self, feed_id: S) -> Result<Vec<FeedItem>>
where
S: AsRef<str>;
fn read_items_with_options<S>(
&self,
feed_id: S,
options: &ReadOptions,
) -> Result<Vec<FeedItem>>
where
S: AsRef<str>;
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct PingResponse {
pub code: u16,
pub message: String,
}
pub async fn ping() -> Result<PingResponse> {
let base_url = env_or_default_url()?;
let token = api_token()?;
let http_client = reqwest::Client::new();
ping_with_args(&http_client, base_url, token).await
}
pub async fn ping_bool() -> bool {
ping().await.is_ok()
}
pub async fn ping_with_args<S>(
http_client: &reqwest::Client,
base_url: S,
token: S,
) -> Result<PingResponse>
where
S: AsRef<str>,
{
let full_url = format!("{}ping/", base_url.as_ref());
let (code, text) = api_get(http_client, &full_url, token.as_ref()).await?;
if code == 200 {
Ok(json_from_str(&text)?)
} else {
Err(api_error(code, &text))
}
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct NewInputItemsResponse {
pub code: u16,
pub feed_id: String,
pub message: String,
}
pub async fn new_items(items: &[InputItem]) -> Result<NewInputItemsResponse> {
let base_url = env_or_default_url()?;
let token = api_token()?;
let http_client = reqwest::Client::new();
new_items_with_args(items, &http_client, base_url, token).await
}
pub async fn new_items_with_args<S>(
items: &[InputItem],
http_client: &reqwest::Client,
base_url: S,
token: S,
) -> Result<NewInputItemsResponse>
where
S: AsRef<str>,
{
if items.len() > 10 {
return Err(Error {
kind: Kind::IllegalParameter(format!(
"too many items ({}). See chunking example (new_items_all) to send 10 at a time.",
items.len()
)),
});
}
let data = NewItemsBody {
items: items.to_vec(),
};
let full_url = format!("{}items/", base_url.as_ref());
let (code, text) = api_post(http_client, &full_url, token.as_ref(), &data).await?;
if code == 200 {
Ok(json_from_str(&text)?)
} else {
Err(api_error(code, &text))
}
}
pub async fn new_items_all(items: &[InputItem], sleep_ms: u64) -> Result<String> {
let base_url = env_or_default_url()?;
let token = api_token()?;
let http_client = reqwest::Client::new();
new_items_all_with_args(items, sleep_ms, &http_client, base_url, token).await
}
pub async fn new_items_all_with_args<S>(
items: &[InputItem],
sleep_ms: u64,
http_client: &reqwest::Client,
base_url: S,
token: S,
) -> Result<String>
where
S: AsRef<str>,
{
if sleep_ms < 5 {
return Err(Error {
kind: Kind::IllegalParameter(format!("sleep_ms ({}) must be 5 or more", sleep_ms)),
});
}
let sleep_duration = Duration::from_millis(sleep_ms);
let base_url = base_url.as_ref();
let token = token.as_ref();
let mut feed_id = None;
let mut chunks = items.chunks(10).peekable();
while let Some(chunk) = chunks.next() {
let response = new_items_with_args(chunk, http_client, base_url, token).await?;
if feed_id.is_none() {
feed_id = Some(response.feed_id);
}
if chunks.peek().is_some() {
sleep(sleep_duration).await;
}
}
match feed_id {
None => Err(Error {
kind: Kind::IllegalResult("new items API success(es) without a feed ID".to_string()),
}),
Some(fid) => Ok(fid),
}
}
#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
pub struct NewItemsBody {
items: Vec<InputItem>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ReadOptions {
pub max_items: usize,
pub include_item_content: bool,
pub item_time_after: Option<String>,
pub item_time_before: Option<String>,
}
impl Default for ReadOptions {
fn default() -> Self {
Self {
max_items: 10,
include_item_content: false,
item_time_after: None,
item_time_before: None,
}
}
}
pub async fn read_items<S>(feed_id: S, read_options: Option<&ReadOptions>) -> Result<Vec<FeedItem>>
where
S: AsRef<str>,
{
let base_url = env_or_default_url()?;
let token = api_token()?;
let http_client = reqwest::Client::new();
read_items_with_args(
feed_id.as_ref(),
read_options,
&http_client,
&base_url,
&token,
)
.await
}
pub async fn read_items_with_args<S>(
feed_id: S,
read_options: Option<&ReadOptions>,
http_client: &reqwest::Client,
base_url: S,
token: S,
) -> Result<Vec<FeedItem>>
where
S: AsRef<str>,
{
let feed_id_str = feed_id.as_ref().trim();
if feed_id_str.len() != 45 {
return Err(Error {
kind: Kind::IllegalParameter(format!(
"`feed_id` is expected to be 45 characters ('{}')",
feed_id.as_ref()
)),
});
}
let validated = match read_options.as_ref() {
None => ReadOptions {
..Default::default()
},
Some(given) => validate_read_options(given)?,
};
let mut query = vec![
("max_items", validated.max_items.to_string()),
(
"include_item_content",
validated.include_item_content.to_string(),
),
];
if let Some(item_time_after) = validated.item_time_after {
query.push(("item_time_after", item_time_after));
}
if let Some(item_time_before) = validated.item_time_before {
query.push(("item_time_before", item_time_before));
}
let url = format!("{}feeds/{}/", base_url.as_ref(), feed_id_str);
let (code, text) = api_get_with_query(http_client, &url, &query, token.as_ref()).await?;
let response: ReadFeedItemsResponse = if code == 200 {
json_from_str(&text)?
} else {
return Err(api_error(code, &text));
};
Ok(response.feed_items)
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct ReadFeedItemsResponse {
pub code: u16,
pub feed_items: Vec<FeedItem>,
}
async fn api_get(
http_client: &reqwest::Client,
full_url: &str,
token: &str,
) -> Result<(u16, String)> {
let res = http_client
.get(full_url)
.header(X_AUTH_TOKEN_HEADER, token)
.send()
.await?;
let code = res.status().as_u16();
let text = res.text().await?;
Ok((code, text))
}
async fn api_get_with_query<T>(
http_client: &reqwest::Client,
url: &str,
query: &T,
token: &str,
) -> Result<(u16, String)>
where
T: Serialize + ?Sized,
{
let res = http_client
.get(url)
.header(X_AUTH_TOKEN_HEADER, token)
.query(query)
.send()
.await?;
let code = res.status().as_u16();
let text = res.text().await?;
Ok((code, text))
}
async fn api_post<T>(
http_client: &reqwest::Client,
full_url: &str,
token: &str,
data: &T,
) -> Result<(u16, String)>
where
T: Serialize + ?Sized,
{
let res = http_client
.post(full_url)
.header(X_AUTH_TOKEN_HEADER, token)
.json(data)
.send()
.await?;
let code = res.status().as_u16();
let text = res.text().await?;
Ok((code, text))
}
fn validate_read_options(given: &ReadOptions) -> Result<ReadOptions> {
if given.include_item_content && ((given.max_items < 1) || (given.max_items > 10)) {
return Err(Error {
kind: Kind::IllegalParameter(format!(
"`max_items` must be 1 to 10 when `include_item_content` is true, received {}",
given.max_items
)),
});
}
if (given.max_items < 1) || (given.max_items > 50) {
return Err(Error {
kind: Kind::IllegalParameter(format!(
"`max_items` must be 1 to 50, received {}",
given.max_items
)),
});
}
if given.item_time_after.is_some() && given.item_time_before.is_some() {
return Err(Error {
kind: Kind::IllegalParameter(
"cannot simultaneously query with `item_time_after` and `item_time_before`"
.to_string(),
),
});
}
let item_time_after = match &given.item_time_after {
None => None,
Some(it) => Some(normalize_item_time(it)?),
};
let item_time_before = match &given.item_time_before {
None => None,
Some(it) => Some(normalize_item_time(it)?),
};
Ok(ReadOptions {
max_items: given.max_items,
include_item_content: given.include_item_content,
item_time_after,
item_time_before,
})
}