use std::fmt::{Display, Formatter};
type NoSearchType = ();
#[derive(Debug, Clone)]
pub enum SearchType {
Search,
Project { id: String },
ProjectVersion { id: String },
MultiProject { ids: Vec<&'static str> },
VersionFile { hash: String },
Dependencies { id: String },
Categories,
Loaders,
}
pub struct SearchBuilder<T> {
search_type: T,
facets: Option<Vec<FacetsDisjunction>>,
query: Option<String>,
limit: Option<u32>,
offset: Option<u32>,
game_versions: Vec<String>,
}
impl SearchBuilder<NoSearchType> {
pub fn new() -> SearchBuilder<NoSearchType> {
SearchBuilder {
search_type: (),
facets: None,
limit: None,
offset: None,
query: None,
game_versions: vec![],
}
}
}
impl<T> SearchBuilder<T> {
pub fn facets(mut self, facets: Vec<FacetsDisjunction>) -> Self {
self.facets = Some(facets);
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
pub fn game_versions(mut self, versions: Vec<String>) -> Self {
self.game_versions = versions;
self
}
pub fn add_game_version(mut self, version: &str) -> Self {
self.game_versions
.push(version.to_owned());
self
}
pub fn search_type(self, search_type: SearchType) -> SearchBuilder<SearchType> {
SearchBuilder {
search_type,
query: self.query,
facets: self.facets,
offset: self.offset,
limit: self.limit,
game_versions: self.game_versions,
}
}
}
impl SearchBuilder<SearchType> {
pub fn build_url(self) -> String {
use std::mem::discriminant;
let mut url: String = "https://api.modrinth.com/v2/".to_string();
let component = match &self.search_type {
SearchType::Project { id } => &format!("project/{id}?"),
SearchType::MultiProject { ids } => {
let ids = ids
.iter()
.map(|id| format!("\"{id}\""))
.collect::<Vec<String>>()
.join(",");
&format!("projects?ids=[{ids}]")
}
SearchType::Search => "search?",
SearchType::VersionFile { hash } => &format!("version_file/{hash}"),
SearchType::Dependencies { .. } => todo!(),
SearchType::Categories => {
url.push_str("tag/category");
return url;
}
SearchType::Loaders => {
url.push_str("tag/loader");
return url;
}
SearchType::ProjectVersion { id } => &format!("project/{id}/version"),
};
url.push_str(component);
if !self.game_versions.is_empty()
&& discriminant(&self.search_type)
== discriminant(&SearchType::ProjectVersion { id: "".to_string() })
{
url.push('?');
url.push_str("game_versions=[");
for version in self.game_versions {
url.push_str(&format!("\"{version}\","))
}
url.pop();
url.push(']');
return url;
}
if let Some(query) = self.query {
url.push_str(format!("query={query}&").as_str())
}
if let Some(limit) = self.limit {
url.push_str(format!("limit={limit}&").as_str())
}
if let Some(offset) = self.offset {
url.push_str(format!("offset={offset}&").as_str())
}
if let Some(facets) = self.facets {
url.push_str("facets=[");
for conjunction in facets {
url.push_str("[");
for face in conjunction.facets {
url.push_str(format!("{face},").as_str())
}
url.pop();
url.push_str("],");
}
url.pop();
url.push(']');
url.push('&');
}
if url.ends_with('&') {
url.pop();
}
url
}
}
#[derive(Debug, Clone)]
pub struct FacetsDisjunction {
facets: Vec<Facets>,
}
impl FacetsDisjunction {
pub fn new() -> Self {
Self { facets: vec![] }
}
pub fn push(&mut self, facet: Facets) {
self.facets.push(facet)
}
}
#[derive(Debug, Clone)]
pub enum Facets {
ProjectType(String),
Categories(String),
Version(String),
ClientSide(Requirement),
ServerSide(Requirement),
OpenSource,
}
#[derive(Debug, Copy, Clone)]
pub enum Requirement {
Optional,
Required,
Unsupported,
}
impl Display for Requirement {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Optional => "optional",
Self::Required => "required",
Self::Unsupported => "unsupported",
};
f.write_str(s)
}
}
impl Display for Facets {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
Facets::ProjectType(t) => format!("\"project_type:{t}\""),
Facets::Categories(c) => format!("\"categories:{c}\""),
Facets::Version(v) => format!("\"versions:{v}\""),
Facets::ClientSide(r) => format!("\"client_side:{r}\""),
Facets::ServerSide(r) => format!("\"server_side:{r}\""),
Facets::OpenSource => todo!(),
};
f.write_str(s.as_str())
}
}
#[cfg(test)]
mod tests {
use mine_data_structs::rinth::RinthCategories;
use reqwest::{ClientBuilder, Method};
use super::*;
use crate::searcher::rinth::SearchType::Categories;
#[test]
pub fn search_builder() {
let url = SearchBuilder::new()
.offset(10)
.limit(5)
.search_type(SearchType::Search)
.build_url();
assert_eq!("https://api.modrinth.com/v2/search?limit=5&offset=10", url)
}
#[test]
pub fn search_builder_facets() {
let mut versions_facets = FacetsDisjunction::new();
versions_facets.push(Facets::Version("1.21".to_string()));
versions_facets.push(Facets::Version("1.20".to_string()));
let url = SearchBuilder::new()
.offset(10)
.limit(5)
.facets(vec![versions_facets])
.search_type(SearchType::Search)
.build_url();
assert_eq!(
"https://api.modrinth.com/v2/search?limit=5&offset=10&facets=[\
[\"versions:1.21\",\"versions:1.20\"]\
]",
url
);
}
#[test]
pub fn search_builder_facets_disjunction() {
let mut versions_facets = FacetsDisjunction::new();
versions_facets.push(Facets::Version("1.21".to_string()));
versions_facets.push(Facets::Version("1.20".to_string()));
let mut type_facets = FacetsDisjunction::new();
type_facets.push(Facets::ProjectType("modpack".to_string()));
let url = SearchBuilder::new()
.offset(10)
.limit(5)
.facets(vec![versions_facets, type_facets])
.search_type(SearchType::Search)
.build_url();
assert_eq!(
"https://api.modrinth.com/v2/search?limit=5&offset=10&facets=[\
[\"versions:1.21\",\"versions:1.20\"],\
[\"project_type:modpack\"]\
]",
url
);
}
#[test]
pub fn search_builder_facets_disjunction2() {
let mut versions_facets = FacetsDisjunction::new();
versions_facets.push(Facets::Version("1.19".to_string()));
versions_facets.push(Facets::Version("1.22".to_string()));
let mut type_facets = FacetsDisjunction::new();
type_facets.push(Facets::ProjectType("modpack".to_string()));
let mut categories_facets = FacetsDisjunction::new();
categories_facets.push(Facets::Categories("technology".to_string()));
categories_facets.push(Facets::Categories("adventure".to_string()));
let url = SearchBuilder::new()
.offset(10)
.limit(5)
.facets(vec![versions_facets, type_facets, categories_facets])
.search_type(SearchType::Search)
.build_url();
assert_eq!(
"https://api.modrinth.com/v2/search?limit=5&offset=10&facets=[\
[\"versions:1.19\",\"versions:1.22\"],\
[\"project_type:modpack\"],\
[\"categories:technology\",\"categories:adventure\"]\
]",
url
);
}
#[test]
pub fn search_builder_projects() {
let url = SearchBuilder::new()
.search_type(SearchType::MultiProject {
ids: vec!["AAA", "BBB"],
})
.build_url();
assert_eq!(
"https://api.modrinth.com/v2/projects?ids=[\"AAA\",\"BBB\"]",
url
)
}
#[test]
pub fn search_builder_project_versions() {
let url = SearchBuilder::new()
.search_type(SearchType::ProjectVersion {
id: "Jw3Wx1KR".to_string(),
})
.game_versions(vec!["1.18".to_string(), "1.18.2".to_string()])
.build_url();
assert_eq!(
"https://api.modrinth.com/v2/project/Jw3Wx1KR/version?game_versions=[\"1.18\",\"1.18.2\"]",
url
);
}
#[tokio::test]
pub async fn search_categories() {
let url = SearchBuilder::new()
.search_type(Categories)
.build_url();
let categories: RinthCategories = ClientBuilder::new()
.build()
.unwrap()
.get(&url)
.header(
"User-Agent",
"sergious234/uranium-rs (tests)/ (sergious234@gmail.com)",
)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
println!("{:?}", categories);
assert!(!categories.is_empty())
}
#[test]
pub fn request_builder() {
let c = reqwest::Client::new()
.request(Method::GET, "https://api.modrinth.com/v2/search")
.query(&[("query", "pokemon")])
.query(&[("offset", 10)])
.query(&[("limit", 100)])
.build()
.unwrap();
assert_eq!(
c.url().as_str(),
"https://api.modrinth.com/v2/search?query=pokemon&offset=10&limit=100"
)
}
}