Skip to content

Commit

Permalink
Add WordPressOrgAPIClient to list plugins and get a specific plugin (#…
Browse files Browse the repository at this point in the history
…445)

* Export plugin directory types as uniffi types

* Add WordPressOrgAPIClient to list plugins and get a specific plugin

* Implement browse and search plugins

* Update test assertions

Co-authored-by: Oguz Kocer <[email protected]>

* Update test assertions

Co-authored-by: Oguz Kocer <[email protected]>

* Reuse AsyncWpNetworking

* Remove Result<T>

* Extract building request functions

* Remove tokio from unit tests

* Rename an enum variant name

The name 'List' causes compiling issues on the kotlin binding

* Change some functions to associated functions

---------

Co-authored-by: Oguz Kocer <[email protected]>
  • Loading branch information
crazytonyli and oguzkocer authored Dec 18, 2024
1 parent bfb228e commit 8469393
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 48 deletions.
11 changes: 11 additions & 0 deletions wp_api/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ impl WpNetworkRequest {
}
}

impl WpNetworkRequest {
pub fn get(url: WpEndpointUrl) -> Self {
Self {
method: RequestMethod::GET,
url,
header_map: Arc::new(WpNetworkHeaderMap::default()),
body: None,
}
}
}

impl Debug for WpNetworkRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = format!(
Expand Down
204 changes: 204 additions & 0 deletions wp_api/src/wordpress_org/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use crate::{
request::{endpoint::WpEndpointUrl, RequestExecutor, WpNetworkRequest, WpNetworkResponse},
RequestExecutionError,
};
use serde::de::DeserializeOwned;
use std::{result::Result, sync::Arc};
use url::Url;

use super::plugin_directory::{PluginInformation, QueryPluginResponse};

#[derive(Debug, uniffi::Object)]
pub struct WordPressOrgApiClient {
pub(crate) request_executor: Arc<dyn RequestExecutor>,
}

#[uniffi::export]
impl WordPressOrgApiClient {
#[uniffi::constructor]
pub fn new(request_executor: Arc<dyn RequestExecutor>) -> Self {
Self { request_executor }
}

pub async fn plugin_information(
&self,
slug: &str,
) -> Result<PluginInformation, WordPressOrgApiClientError> {
self.execute(Self::plugin_information_request(slug)).await
}

pub async fn browse_plugins(
&self,
category: Option<WordPressOrgApiPluginDirectoryCategory>,
page: u64,
page_size: u64,
) -> Result<QueryPluginResponse, WordPressOrgApiClientError> {
let request = Self::browse_plugins_request(category, page, page_size);
self.execute(request).await
}

pub async fn search_plugins(
&self,
search: String,
page: u64,
page_size: u64,
) -> Result<QueryPluginResponse, WordPressOrgApiClientError> {
let request = Self::search_plugins_request(search, page, page_size);
self.execute(request).await
}
}

impl WordPressOrgApiClient {
pub fn browse_plugins_request(
category: Option<WordPressOrgApiPluginDirectoryCategory>,
page: u64,
page_size: u64,
) -> WpNetworkRequest {
Self::query_plugins_request(page, page_size, |url| match category {
Some(category) => {
let mut url = url;
url.query_pairs_mut()
.append_pair("browse", category.as_str());
url
}
None => url,
})
}

pub fn search_plugins_request(search: String, page: u64, page_size: u64) -> WpNetworkRequest {
Self::query_plugins_request(page, page_size, |url| {
let mut url = url;
url.query_pairs_mut().append_pair("search", &search);
url
})
}

fn plugin_information_request(slug: &str) -> WpNetworkRequest {
let mut url = Self::plugin_info_api_url();
url.query_pairs_mut()
.append_pair("action", "plugin_information")
.append_pair("fields", "icons")
.append_pair("slug", slug);
WpNetworkRequest::get(WpEndpointUrl(url.to_string()))
}

fn query_plugins_request<F>(page: u64, page_size: u64, url_builder: F) -> WpNetworkRequest
where
F: FnOnce(Url) -> Url,
{
let mut url = Self::plugin_info_api_url();
url.query_pairs_mut()
.append_pair("action", "query_plugins")
.append_pair("page", &page.to_string())
.append_pair("per_page", &page_size.to_string());
let url = url_builder(url);
WpNetworkRequest::get(WpEndpointUrl(url.to_string()))
}

fn plugin_info_api_url() -> Url {
Url::parse("https://api.wordpress.org/plugins/info/1.2/").expect("The URL is valid")
}

async fn execute<T>(&self, request: WpNetworkRequest) -> Result<T, WordPressOrgApiClientError>
where
T: DeserializeOwned,
{
let response = self.request_executor.execute(Arc::new(request)).await?;
Self::parse(response)
}

fn parse<T>(response: WpNetworkResponse) -> Result<T, WordPressOrgApiClientError>
where
T: DeserializeOwned,
{
match response.status_code {
200 => serde_json::from_slice(&response.body).map_err(|e| {
WordPressOrgApiClientError::ResponseParsingError {
reason: format!("Failed to parse response body as JSON: {}", e),
response: String::from_utf8_lossy(&response.body).to_string(),
}
}),
_ => Err(WordPressOrgApiClientError::UnexpectedStatusCodeError {
status_code: response.status_code,
response: String::from_utf8_lossy(&response.body).to_string(),
}),
}
}
}

#[derive(Debug, PartialEq, Eq, uniffi::Enum)]
pub enum WordPressOrgApiPluginDirectoryCategory {
New,
Popular,
Updated,
TopRated,
}

impl WordPressOrgApiPluginDirectoryCategory {
pub fn as_str(&self) -> &'static str {
match self {
WordPressOrgApiPluginDirectoryCategory::New => "new",
WordPressOrgApiPluginDirectoryCategory::Popular => "popular",
WordPressOrgApiPluginDirectoryCategory::Updated => "updated",
WordPressOrgApiPluginDirectoryCategory::TopRated => "top-rated",
}
}
}

#[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Error)]
pub enum WordPressOrgApiClientError {
#[error(
"Request execution failed!\nStatus Code: '{:?}'.\nResponse: '{}'",
status_code,
reason
)]
RequestExecutionFailed {
status_code: Option<u16>,
reason: String,
},
#[error("Error while parsing. \nReason: {}\nResponse: {}", reason, response)]
ResponseParsingError { reason: String, response: String },
#[error(
"Received a response with an unexpected status code. \nStatus code: {}\nResponse: {}",
status_code,
response
)]
UnexpectedStatusCodeError { status_code: u16, response: String },
}

impl From<RequestExecutionError> for WordPressOrgApiClientError {
fn from(e: RequestExecutionError) -> Self {
match e {
RequestExecutionError::RequestExecutionFailed {
status_code,
reason,
} => WordPressOrgApiClientError::RequestExecutionFailed {
status_code,
reason,
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_plugin_info_requests_include_icons() {
let request = WordPressOrgApiClient::plugin_information_request("akismet");
assert!(request.url.0.contains("fields=icons"));
}

#[test]
fn test_search_does_not_include_pagination() {
let request = WordPressOrgApiClient::search_plugins_request("akismet".to_string(), 3, 24);

// The 'request[x]' parameters do not work for the search endpoint.
// The 'page' and 'per_page' parameters do.
assert!(!request.url.0.contains("request[page]"));
assert!(!request.url.0.contains("request[per_page]"));
assert!(request.url.0.contains("page=3"));
assert!(request.url.0.contains("per_page=24"));
}
}
2 changes: 2 additions & 0 deletions wp_api/src/wordpress_org/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
mod de;

pub mod client;
pub mod plugin_directory;
22 changes: 11 additions & 11 deletions wp_api/src/wordpress_org/plugin_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{collections::HashMap, fmt::Debug};

use super::de::deserialize_default_values;

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Record)]
pub struct PluginInformation {
pub name: String,
pub slug: String,
Expand Down Expand Up @@ -64,14 +64,14 @@ pub struct PluginInformation {
pub preview_link: String,
}

#[derive(Deserialize, Debug, Eq, PartialEq)]
#[derive(Deserialize, Debug, Eq, PartialEq, uniffi::Record)]
pub struct ContributorDetails {
pub profile: String,
pub avatar: String,
pub display_name: String,
}

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Record)]
pub struct Ratings {
#[serde(rename = "5")]
pub five_star: u32,
Expand All @@ -86,34 +86,34 @@ pub struct Ratings {
}

/// https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#screenshots
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Enum)]
#[serde(untagged)]
pub enum Screenshots {
Named(HashMap<String, Screenshot>),
List(Vec<Screenshot>),
Unnamed(Vec<Screenshot>),
}

impl Default for Screenshots {
fn default() -> Self {
Screenshots::List(vec![])
Screenshots::Unnamed(vec![])
}
}

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Record)]
pub struct Screenshot {
pub src: String,
pub caption: String,
}

#[derive(Deserialize, Debug, Eq, PartialEq, Default)]
#[derive(Deserialize, Debug, Eq, PartialEq, Default, uniffi::Record)]
pub struct Banners {
#[serde(deserialize_with = "deserialize_default_values")]
pub low: String,
#[serde(deserialize_with = "deserialize_default_values")]
pub high: String,
}

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Record)]
pub struct Icons {
#[serde(rename = "1x")]
pub low: Option<String>,
Expand All @@ -123,13 +123,13 @@ pub struct Icons {
pub default: Option<String>,
}

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Record)]
pub struct QueryPluginResponse {
pub info: QueryPluginResponseInfo,
pub plugins: Vec<PluginInformation>,
}

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, uniffi::Record)]
pub struct QueryPluginResponseInfo {
pub page: u64,
pub pages: u64,
Expand Down
Loading

0 comments on commit 8469393

Please sign in to comment.