mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use self::error::{ApiError, ApiErrorKind};
use self::models::{
AccessTokenResponse, Category, Counts, Entry, FeedlyError, Profile, ProfileUpdate,
RefreshTokenResponse, SearchResult, Stream, Subscription, SubscriptionInput, Tag,
};
use chrono::{DateTime, Duration, Utc};
use failure::ResultExt;
use log::info;
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Client, StatusCode};
use serde_json::json;
use std::sync::Arc;
use std::sync::Mutex;
use url::Url;
pub type AuthCode = String;
pub type AccessToken = String;
pub type RefreshToken = String;
const FEEDLY_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`')
.add(b'#')
.add(b'?')
.add(b'{')
.add(b'}')
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b']')
.add(b'\\')
.add(b'^')
.add(b'|')
.add(b'+');
pub struct FeedlyApi {
base_uri: Url,
client_id: String,
client_secret: String,
user_id: Arc<Mutex<Option<String>>>,
access_token: Arc<Mutex<AccessToken>>,
refresh_token: Arc<Mutex<RefreshToken>>,
token_expires: Arc<Mutex<DateTime<Utc>>>,
}
impl FeedlyApi {
pub fn new(
client_id: String,
client_secret: String,
access_token: AccessToken,
refresh_token: RefreshToken,
token_expires: DateTime<Utc>,
) -> Result<FeedlyApi, ApiError> {
let api = FeedlyApi {
base_uri: Self::base_uri()?,
client_id,
client_secret,
user_id: Arc::new(Mutex::new(None)),
access_token: Arc::new(Mutex::new(access_token)),
refresh_token: Arc::new(Mutex::new(refresh_token)),
token_expires: Arc::new(Mutex::new(token_expires)),
};
Ok(api)
}
pub fn login_url(client_id: &str, client_secret: &str) -> Result<Url, ApiError> {
let mut url = Self::base_uri()?.as_str().to_owned();
let auth_scope = Self::auth_scope()?.as_str().to_owned();
let redirect_url = Self::redirect_uri()?.as_str().to_owned();
url.push_str("v3/auth/auth");
url.push_str(&format!("?client_secret={}", client_secret));
url.push_str(&format!("&client_id={}", client_id));
url.push_str(&format!("&redirect_uri={}", redirect_url));
url.push_str(&format!("&scope={}", auth_scope));
url.push_str("&response_type=code");
url.push_str("&state=getting_code");
let url = Url::parse(&url).context(ApiErrorKind::Url)?;
Ok(url)
}
pub fn parse_redirected_url(url: Url) -> Result<AuthCode, ApiError> {
if let Some(code) = url.query_pairs().find(|ref x| x.0 == "code") {
return Ok(code.1.to_string());
}
if let Some(error) = url.query_pairs().find(|ref x| x.0 == "error") {
if error.1 == "access_denied" {
return Err(ApiErrorKind::AccessDenied.into());
};
}
Err(ApiErrorKind::Unknown.into())
}
pub fn redirect_uri() -> Result<Url, ApiError> {
let url = Url::parse("http://localhost").context(ApiErrorKind::Url)?;
Ok(url)
}
fn auth_scope() -> Result<Url, ApiError> {
let url =
Url::parse("https://cloud.feedly.com/subscriptions").context(ApiErrorKind::Url)?;
Ok(url)
}
fn base_uri() -> Result<Url, ApiError> {
let url = Url::parse("https://cloud.feedly.com").context(ApiErrorKind::Url)?;
Ok(url)
}
pub async fn initialize_user_id(&self, client: &Client) -> Result<(), ApiError> {
let user_id = {
(*self
.user_id
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)?)
.clone()
};
if user_id.is_none() {
let profile = self.get_profile(client).await?;
{
*self
.user_id
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)? = Some(profile.id);
}
}
Ok(())
}
pub fn parse_expiration_date(expires_in: &str) -> Result<DateTime<Utc>, ApiError> {
let timestamp = expires_in.parse::<i64>().context(ApiErrorKind::Input)?;
let now = Utc::now();
let expires_datetime = now + Duration::seconds(timestamp);
Ok(expires_datetime)
}
pub fn gernerate_feed_id(url: Url) -> String {
format!("feed/{}", url.as_str())
}
pub async fn generate_category_id(
&self,
title: &str,
client: &Client,
) -> Result<String, ApiError> {
self.initialize_user_id(client).await?;
let user_id = {
(*self
.user_id
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)?)
.clone()
};
if let Some(user_id) = user_id {
return Ok(format!("user/{}/category/{}", user_id, title));
}
Err(ApiErrorKind::Unknown.into())
}
pub async fn generate_tag_id(&self, title: &str, client: &Client) -> Result<String, ApiError> {
self.initialize_user_id(client).await?;
let user_id = {
(*self
.user_id
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)?)
.clone()
};
if let Some(user_id) = user_id {
return Ok(format!("user/{}/tag/{}", user_id, title));
}
Err(ApiErrorKind::Unknown.into())
}
pub async fn category_all(&self, client: &Client) -> Result<String, ApiError> {
self.generate_category_id("global.all", client).await
}
pub async fn tag_marked(&self, client: &Client) -> Result<String, ApiError> {
self.generate_tag_id("global.saved", client).await
}
pub async fn tag_read(&self, client: &Client) -> Result<String, ApiError> {
self.generate_tag_id("global.read", client).await
}
pub async fn request_auth_token(
client_id: &str,
client_secret: &str,
auth_code: AuthCode,
client: &Client,
) -> Result<AccessTokenResponse, ApiError> {
let input = json!(
{
"code" : auth_code,
"client_id" : client_id,
"client_secret" : client_secret,
"redirect_uri" : Self::redirect_uri()?.as_str(),
"state" : "feedly-api rust crate",
"grant_type" : "authorization_code"
}
);
let api_endpoint = Self::base_uri()
.context(ApiErrorKind::Url)?
.join("/v3/auth/token")
.context(ApiErrorKind::Url)?;
let response = client
.post(api_endpoint)
.json(&input)
.send()
.await
.context(ApiErrorKind::Http)?
.text()
.await
.context(ApiErrorKind::Http)?;
let response: AccessTokenResponse =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(response)
}
pub async fn refresh_auth_token(
&self,
client: &Client,
) -> Result<RefreshTokenResponse, ApiError> {
let refresh_token = {
(*self
.refresh_token
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)?)
.clone()
};
let input = json!(
{
"refresh_token" : refresh_token,
"client_id" : self.client_id,
"client_secret" : self.client_secret,
"grant_type" : "refresh_token"
}
);
let api_endpoint = self
.base_uri
.clone()
.join("/v3/auth/token")
.context(ApiErrorKind::Url)?;
let response = client
.post(api_endpoint)
.json(&input)
.send()
.await
.context(ApiErrorKind::Http)?
.text()
.await
.context(ApiErrorKind::Http)?;
let response: RefreshTokenResponse =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
info!("Feedly refresh token: {:?}", response);
{
*self
.access_token
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)? = response.access_token.clone();
*self
.token_expires
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)? =
Utc::now() + Duration::seconds(response.expires_in as i64);
}
Ok(response)
}
async fn get_access_token(&self) -> Result<AccessToken, ApiError> {
let expires_in = {
*self
.token_expires
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)?
};
let duration = expires_in.signed_duration_since(Utc::now());
let expired = duration.num_seconds() <= 60;
if !expired {
let access_token = {
(*self
.access_token
.lock()
.map_err(|_e| ApiErrorKind::InternalMutabilty)?)
.clone()
};
return Ok(access_token);
}
Err(ApiErrorKind::TokenExpired.into())
}
async fn post_request(
&self,
json: serde_json::Value,
api_endpoint: &str,
client: &Client,
) -> Result<String, ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self
.base_uri
.clone()
.join(api_endpoint)
.context(ApiErrorKind::Url)?;
let response = client
.post(api_endpoint)
.header(AUTHORIZATION, token)
.json(&json)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: FeedlyError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
return Err(ApiErrorKind::Feedly(error).into());
}
Ok(response)
}
async fn get_request(&self, api_endpoint: &str, client: &Client) -> Result<String, ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self
.base_uri
.clone()
.join(api_endpoint)
.context(ApiErrorKind::Url)?;
let response = client
.get(api_endpoint)
.header(AUTHORIZATION, token)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: FeedlyError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
return Err(ApiErrorKind::Feedly(error).into());
}
Ok(response)
}
async fn put_request(
&self,
json: serde_json::Value,
api_endpoint: &str,
client: &Client,
) -> Result<String, ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self
.base_uri
.clone()
.join(api_endpoint)
.context(ApiErrorKind::Url)?;
let response = client
.put(api_endpoint)
.header(AUTHORIZATION, token)
.json(&json)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: FeedlyError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
return Err(ApiErrorKind::Feedly(error).into());
}
Ok(response)
}
async fn delete_request(&self, api_endpoint: &str, client: &Client) -> Result<(), ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self
.base_uri
.clone()
.join(api_endpoint)
.context(ApiErrorKind::Url)?;
let response = client
.delete(api_endpoint)
.header(AUTHORIZATION, token)
.send()
.await
.context(ApiErrorKind::Http)?;
if response.status() != StatusCode::OK {
let response = response.text().await.context(ApiErrorKind::Http)?;
let error: FeedlyError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
return Err(ApiErrorKind::Feedly(error).into());
}
Ok(())
}
pub async fn get_profile(&self, client: &Client) -> Result<Profile, ApiError> {
let response = self.get_request("/v3/profile", client).await?;
let profile: Profile = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(profile)
}
#[allow(clippy::too_many_arguments)]
pub async fn update_profile(
&self,
client: &Client,
email: Option<String>,
given_name: Option<String>,
family_name: Option<String>,
picture: Option<String>,
gender: Option<bool>,
locale: Option<String>,
twitter: Option<String>,
facebook: Option<String>,
) -> Result<Profile, ApiError> {
let update = ProfileUpdate {
email,
given_name,
family_name,
picture,
gender,
locale,
twitter,
facebook,
};
let update = serde_json::to_value(update).context(ApiErrorKind::Json)?;
let response = self.post_request(update, "/v3/profile", client).await?;
let profile: Profile = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(profile)
}
pub async fn get_categories(&self, client: &Client) -> Result<Vec<Category>, ApiError> {
let response = self
.get_request("/v3/categories?sort=feedly", client)
.await?;
let category_vec: Vec<Category> =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(category_vec)
}
pub async fn update_category(
&self,
id: &str,
label: &str,
client: &Client,
) -> Result<(), ApiError> {
let input = json!(
{
"label" : label,
}
);
let id = utf8_percent_encode(&id, FEEDLY_ENCODE_SET).to_string();
let endpoint = FeedlyApi::category_api_endpoint(&id);
let _ = self.post_request(input, &endpoint, client).await?;
Ok(())
}
pub async fn delete_category(&self, id: &str, client: &Client) -> Result<(), ApiError> {
let id = utf8_percent_encode(&id, FEEDLY_ENCODE_SET).to_string();
let endpoint = FeedlyApi::category_api_endpoint(&id);
self.delete_request(&endpoint, client).await?;
Ok(())
}
pub async fn get_subsriptions(&self, client: &Client) -> Result<Vec<Subscription>, ApiError> {
let response = self.get_request("/v3/subscriptions", client).await?;
let subscription_vec: Vec<Subscription> =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(subscription_vec)
}
pub async fn add_subscription(
&self,
subscription: SubscriptionInput,
client: &Client,
) -> Result<(), ApiError> {
let json = serde_json::to_value(subscription).context(ApiErrorKind::Json)?;
let _ = self.post_request(json, "/v3/subscriptions", client).await?;
Ok(())
}
pub async fn update_subscriptions(
&self,
subscriptions: Vec<SubscriptionInput>,
client: &Client,
) -> Result<(), ApiError> {
let json = serde_json::to_value(subscriptions).context(ApiErrorKind::Json)?;
let _ = self
.post_request(json, "/v3/subscriptions/.mput", client)
.await?;
Ok(())
}
pub async fn delete_subscription(&self, id: &str, client: &Client) -> Result<(), ApiError> {
let id = utf8_percent_encode(&id, FEEDLY_ENCODE_SET).to_string();
let api_endpoint = FeedlyApi::subscription_api_endpoint(&id);
self.delete_request(&api_endpoint, client).await?;
Ok(())
}
pub async fn get_tags(&self, client: &Client) -> Result<Vec<Tag>, ApiError> {
let response = self.get_request("/v3/tags", client).await?;
let tag_vec: Vec<Tag> = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(tag_vec)
}
fn category_api_endpoint(category_id: &str) -> String {
let mut api_endpoint = String::from("/v3/categories/");
api_endpoint.push_str(category_id);
api_endpoint
}
fn subscription_api_endpoint(subscription_id: &str) -> String {
let mut api_endpoint = String::from("/v3/subscriptions/");
api_endpoint.push_str(subscription_id);
api_endpoint
}
fn tag_api_endpoint(
tag_ids: Vec<&str>,
entry_ids: Option<Vec<&str>>,
) -> Result<String, ApiError> {
if tag_ids.is_empty() {
return Err(ApiErrorKind::Input.into());
}
let mut api_endpoint = String::from("/v3/tags/");
for tag_id in tag_ids {
let tag_id = utf8_percent_encode(tag_id, FEEDLY_ENCODE_SET).to_string();
api_endpoint.push_str(&tag_id);
api_endpoint.push_str(",");
}
api_endpoint = api_endpoint[..api_endpoint.len() - 1].to_owned();
if let Some(entry_ids) = entry_ids {
if entry_ids.is_empty() {
return Err(ApiErrorKind::Input.into());
}
api_endpoint.push_str("/");
for entry_id in entry_ids {
let entry_id = utf8_percent_encode(&entry_id, FEEDLY_ENCODE_SET).to_string();
api_endpoint.push_str(&entry_id);
api_endpoint.push_str(",");
}
api_endpoint = api_endpoint[..api_endpoint.len() - 1].to_owned();
}
Ok(api_endpoint)
}
pub async fn tag_entry(
&self,
entry_id: &str,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"entryId" : entry_id,
}
);
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
let _ = self.put_request(json, &api_endpoint, client).await?;
Ok(())
}
pub async fn tag_entries(
&self,
entry_ids: Vec<&str>,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"entryIds" : entry_ids,
}
);
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
let _ = self.put_request(json, &api_endpoint, client).await?;
Ok(())
}
pub async fn update_tag(
&self,
tag_id: &str,
label: &str,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"label" : label,
}
);
let api_endpoint = FeedlyApi::tag_api_endpoint(vec![tag_id], None)?;
let _ = self.post_request(json, &api_endpoint, client).await?;
Ok(())
}
pub async fn untag_entries(
&self,
entry_ids: Vec<&str>,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, Some(entry_ids))?;
self.delete_request(&api_endpoint, client).await?;
Ok(())
}
pub async fn delete_tags(&self, tag_ids: Vec<&str>, client: &Client) -> Result<(), ApiError> {
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
self.delete_request(&api_endpoint, client).await?;
Ok(())
}
pub async fn get_entries(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<Vec<Entry>, ApiError> {
let json = serde_json::to_value(entry_ids).context(ApiErrorKind::Json)?;
let response = self.post_request(json, "/v3/entries/.mget", client).await?;
let entry_vec: Vec<Entry> = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(entry_vec)
}
pub async fn create_entry(
&self,
entry: Entry,
client: &Client,
) -> Result<Vec<String>, ApiError> {
let json = serde_json::to_value(entry).context(ApiErrorKind::Json)?;
let response = self.post_request(json, "/v3/entries/", client).await?;
let entry_ids: Vec<String> = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(entry_ids)
}
fn stream_api_endpoint(
stream_id: &str,
continuation: Option<String>,
count: Option<u32>,
ranked: Option<&str>,
unread_only: Option<bool>,
newer_than: Option<u64>,
) -> String {
let mut api_endpoint = String::from("/v3/streams/contents?streamId=");
let stream_id = utf8_percent_encode(&stream_id, FEEDLY_ENCODE_SET).to_string();
api_endpoint.push_str(&stream_id);
if let Some(continuation) = continuation {
api_endpoint.push_str(&format!("&continuation={}", continuation));
}
if let Some(count) = count {
api_endpoint.push_str(&format!("&count={}", count));
}
if let Some(ranked) = ranked {
api_endpoint.push_str(&format!("&ranked={}", ranked));
}
if let Some(unread_only) = unread_only {
api_endpoint.push_str(&format!("&unreadOnly={}", unread_only));
}
if let Some(newer_than) = newer_than {
api_endpoint.push_str(&format!("&newerThan={}", newer_than));
}
api_endpoint
}
#[allow(clippy::too_many_arguments)]
pub async fn get_stream(
&self,
stream_id: &str,
continuation: Option<String>,
count: Option<u32>,
ranked: Option<&str>,
unread_only: Option<bool>,
newer_than: Option<u64>,
client: &Client,
) -> Result<Stream, ApiError> {
let api_endpoint = FeedlyApi::stream_api_endpoint(
stream_id,
continuation,
count,
ranked,
unread_only,
newer_than,
);
let response = self.get_request(&api_endpoint, client).await?;
let stream: Stream = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(stream)
}
pub async fn get_unread_counts(&self, client: &Client) -> Result<Counts, ApiError> {
let response = self.get_request("/v3/markers/counts", client).await?;
let counts: Counts = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(counts)
}
pub async fn mark_entries_read(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_entries_unread(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "keepUnread",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_feeds_read(
&self,
feed_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "feeds",
"feedIds" : feed_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_categories_read(
&self,
category_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "categories",
"categoryIds" : category_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_tags_read(
&self,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "tags",
"tagIds" : tag_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_entries_saved(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsSaved",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_entries_unsaved(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsUnsaved",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(json, "/v3/markers", client).await?;
Ok(())
}
#[allow(dead_code)]
pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
self.get_request("/v3/opml", client).await
}
pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self
.base_uri
.clone()
.join("/v3/opml")
.context(ApiErrorKind::Url)?;
let response = client
.post(api_endpoint)
.header(AUTHORIZATION, token)
.header(CONTENT_TYPE, "text/xml")
.body(opml.to_owned())
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: FeedlyError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
return Err(ApiErrorKind::Feedly(error).into());
}
Ok(())
}
pub async fn search_feedly_cloud(
client: &Client,
query: &str,
count: Option<u32>,
locale: Option<&str>,
) -> Result<SearchResult, ApiError> {
let mut query = format!(
"/v3/search/feeds?query={}",
utf8_percent_encode(&query, FEEDLY_ENCODE_SET)
);
if let Some(count) = count {
query.push_str(&format!("&count={}", count));
}
if let Some(locale) = locale {
let locale = utf8_percent_encode(&locale, FEEDLY_ENCODE_SET).to_string();
query.push_str(&format!("&locale={}", locale));
}
let api_endpoint = Self::base_uri()
.context(ApiErrorKind::Url)?
.join(&query)
.context(ApiErrorKind::Url)?;
let response = client
.get(api_endpoint)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: FeedlyError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
return Err(ApiErrorKind::Feedly(error).into());
}
let result: SearchResult = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(result)
}
}