Side types overhaul (#762)

* side types overhaul

* fixes, fmt clippy

* migration fix for v3 bug

* fixed migration issues

* more tested migration changes

* fmt, clippy

* bump cicd

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Wyatt Verchere
2023-11-28 10:36:59 -08:00
committed by GitHub
parent fd18185ef0
commit f731c1080d
28 changed files with 957 additions and 555 deletions

View File

@@ -320,6 +320,23 @@ impl LoaderField {
exec: E,
redis: &RedisPool,
) -> Result<Vec<LoaderField>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let found_loader_fields = Self::get_fields_per_loader(loader_ids, exec, redis).await?;
let result = found_loader_fields
.into_values()
.flatten()
.unique_by(|x| x.id)
.collect();
Ok(result)
}
pub async fn get_fields_per_loader<'a, E>(
loader_ids: &[LoaderId],
exec: E,
redis: &RedisPool,
) -> Result<HashMap<LoaderId, Vec<LoaderField>>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
@@ -336,11 +353,11 @@ impl LoaderField {
.filter_map(|x: String| serde_json::from_str::<RedisLoaderFieldTuple>(&x).ok())
.collect();
let mut found_loader_fields = vec![];
let mut found_loader_fields = HashMap::new();
if !cached_fields.is_empty() {
for (loader_id, fields) in cached_fields {
if loader_ids.contains(&loader_id) {
found_loader_fields.extend(fields);
found_loader_fields.insert(loader_id, fields);
loader_ids.retain(|x| x != &loader_id);
}
}
@@ -388,14 +405,10 @@ impl LoaderField {
redis
.set_serialized_to_json(LOADER_FIELDS_NAMESPACE, k.0, (k, &v), None)
.await?;
found_loader_fields.extend(v);
found_loader_fields.insert(k, v);
}
}
let result = found_loader_fields
.into_iter()
.unique_by(|x| x.id)
.collect();
Ok(result)
Ok(found_loader_fields)
}
// Gets all fields for a given loader(s)

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use super::super::ids::OrganizationId;
use super::super::teams::TeamId;
use super::super::users::UserId;
@@ -10,6 +12,7 @@ use crate::models::projects::{
Project, ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
};
use crate::models::threads::ThreadId;
use crate::routes::v2_reroute;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@@ -85,26 +88,6 @@ impl LegacyProject {
let mut loaders = data.loaders;
if let Some(versions_item) = versions_item {
client_side = versions_item
.version_fields
.iter()
.find(|f| f.field_name == "client_side")
.and_then(|f| {
Some(LegacySideType::from_string(
f.value.serialize_internal().as_str()?,
))
})
.unwrap_or(LegacySideType::Unknown);
server_side = versions_item
.version_fields
.iter()
.find(|f| f.field_name == "server_side")
.and_then(|f| {
Some(LegacySideType::from_string(
f.value.serialize_internal().as_str()?,
))
})
.unwrap_or(LegacySideType::Unknown);
game_versions = versions_item
.version_fields
.iter()
@@ -113,6 +96,14 @@ impl LegacyProject {
.map(|v| v.into_iter().map(|v| v.version).collect())
.unwrap_or(Vec::new());
// Extract side types from remaining fields (singleplayer, client_only, etc)
let fields = versions_item
.version_fields
.iter()
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
.collect::<HashMap<_, _>>();
(client_side, server_side) = v2_reroute::convert_side_types_v2(&fields);
// - if loader is mrpack, this is a modpack
// the loaders are whatever the corresponding loader fields are
if versions_item.loaders == vec!["mrpack".to_string()] {
@@ -194,7 +185,7 @@ impl LegacyProject {
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Copy)]
#[serde(rename_all = "kebab-case")]
pub enum LegacySideType {
Required,

View File

@@ -1,4 +1,4 @@
use crate::{models::projects::SideType, util::env::parse_strings_from_var};
use crate::{models::v2::projects::LegacySideType, util::env::parse_strings_from_var};
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -23,7 +23,7 @@ pub struct PackFormat {
pub struct PackFile {
pub path: String,
pub hashes: std::collections::HashMap<PackFileHash, String>,
pub env: Option<std::collections::HashMap<EnvType, SideType>>,
pub env: Option<std::collections::HashMap<EnvType, LegacySideType>>, // TODO: Should this use LegacySideType? Will probably require a overhaul of mrpack format to change this
#[validate(custom(function = "validate_download_url"))]
pub downloads: Vec<String>,
pub file_size: u32,

View File

@@ -216,42 +216,6 @@ pub struct ModeratorMessage {
pub body: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum SideType {
Required,
Optional,
Unsupported,
Unknown,
}
impl std::fmt::Display for SideType {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.as_str())
}
}
impl SideType {
// These are constant, so this can remove unneccessary allocations (`to_string`)
pub fn as_str(&self) -> &'static str {
match self {
SideType::Required => "required",
SideType::Optional => "optional",
SideType::Unsupported => "unsupported",
SideType::Unknown => "unknown",
}
}
pub fn from_string(string: &str) -> SideType {
match string {
"required" => SideType::Required,
"optional" => SideType::Optional,
"unsupported" => SideType::Unsupported,
_ => SideType::Unknown,
}
}
}
pub const DEFAULT_LICENSE_ID: &str = "LicenseRef-All-Rights-Reserved";
#[derive(Serialize, Deserialize, Clone)]

View File

@@ -3,8 +3,8 @@ use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::ids::ImageId;
use crate::models::projects::{DonationLink, Loader, Project, ProjectStatus, SideType};
use crate::models::v2::projects::LegacyProject;
use crate::models::projects::{DonationLink, Loader, Project, ProjectStatus};
use crate::models::v2::projects::{LegacyProject, LegacySideType};
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::default_project_type;
use crate::routes::v3::project_creation::{CreateError, NewGalleryItem};
@@ -60,9 +60,9 @@ struct ProjectCreateData {
pub body: String,
/// The support range for the client project
pub client_side: SideType,
pub client_side: LegacySideType,
/// The support range for the server project
pub server_side: SideType,
pub server_side: LegacySideType,
#[validate(length(max = 32))]
#[validate]
@@ -146,7 +146,7 @@ pub async fn project_create(
let payload = v2_reroute::alter_actix_multipart(
payload,
req.headers().clone(),
|legacy_create: ProjectCreateData| {
|legacy_create: ProjectCreateData| async move {
// Side types will be applied to each version
let client_side = legacy_create.client_side;
let server_side = legacy_create.server_side;
@@ -158,8 +158,7 @@ pub async fn project_create(
.into_iter()
.map(|v| {
let mut fields = HashMap::new();
fields.insert("client_side".to_string(), json!(client_side));
fields.insert("server_side".to_string(), json!(server_side));
fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side));
fields.insert("game_versions".to_string(), json!(v.game_versions));
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.

View File

@@ -3,9 +3,9 @@ use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::projects::{
DonationLink, MonetizationStatus, Project, ProjectStatus, SearchRequest, SideType,
DonationLink, MonetizationStatus, Project, ProjectStatus, SearchRequest, Version,
};
use crate::models::v2::projects::LegacyProject;
use crate::models::v2::projects::{LegacyProject, LegacySideType};
use crate::models::v2::search::LegacySearchResults;
use crate::queue::session::AuthQueue;
use crate::routes::v3::projects::ProjectIds;
@@ -13,10 +13,9 @@ use crate::routes::{v2_reroute, v3, ApiError};
use crate::search::{search_for_project, SearchConfig, SearchError};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use validator::Validate;
@@ -59,27 +58,55 @@ pub async fn project_search(
// Search now uses loader_fields instead of explicit 'client_side' and 'server_side' fields
// While the backend for this has changed, it doesnt affect much
// in the API calls except that 'versions:x' is now 'game_versions:x'
let facets: Option<Vec<Vec<String>>> = if let Some(facets) = info.facets {
let facets = serde_json::from_str::<Vec<Vec<&str>>>(&facets)?;
let facets: Option<Vec<Vec<Vec<String>>>> = if let Some(facets) = info.facets {
let facets = serde_json::from_str::<Vec<Vec<serde_json::Value>>>(&facets)?;
// Search can now *optionally* have a third inner array: So Vec(AND)<Vec(OR)<Vec(AND)< _ >>>
// For every inner facet, we will check if it can be deserialized into a Vec<&str>, and do so.
// If not, we will assume it is a single facet and wrap it in a Vec.
let facets: Vec<Vec<Vec<String>>> = facets
.into_iter()
.map(|facets| {
facets
.into_iter()
.map(|facet| {
if facet.is_array() {
serde_json::from_value::<Vec<String>>(facet).unwrap_or_default()
} else {
vec![serde_json::from_value::<String>(facet.clone())
.unwrap_or_default()]
}
})
.collect_vec()
})
.collect_vec();
// We will now convert side_types to their new boolean format
let facets = v2_reroute::convert_side_type_facets_v3(facets);
Some(
facets
.into_iter()
.map(|facet| {
facet
.into_iter()
.map(|facet| {
let val = match facet.split(':').nth(1) {
Some(val) => val,
None => return facet.to_string(),
};
.map(|facets| {
facets
.into_iter()
.map(|facet| {
let val = match facet.split(':').nth(1) {
Some(val) => val,
None => return facet.to_string(),
};
if facet.starts_with("versions:") {
format!("game_versions:{}", val)
} else if facet.starts_with("project_type:") {
format!("project_types:{}", val)
} else {
facet.to_string()
}
if facet.starts_with("versions:") {
format!("game_versions:{}", val)
} else if facet.starts_with("project_type:") {
format!("project_types:{}", val)
} else {
facet.to_string()
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
})
@@ -279,8 +306,8 @@ pub struct EditProject {
#[validate]
pub donation_urls: Option<Vec<DonationLink>>,
pub license_id: Option<String>,
pub client_side: Option<SideType>,
pub server_side: Option<SideType>,
pub client_side: Option<LegacySideType>,
pub server_side: Option<LegacySideType>,
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
@@ -321,8 +348,8 @@ pub async fn project_edit(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let v2_new_project = new_project.into_inner();
let client_side = v2_new_project.client_side.clone();
let server_side = v2_new_project.server_side.clone();
let client_side = v2_new_project.client_side;
let server_side = v2_new_project.server_side;
let new_slug = v2_new_project.slug.clone();
// TODO: Some kind of handling here to ensure project type is fine.
@@ -376,12 +403,17 @@ pub async fn project_edit(
let version_ids = project_item.map(|x| x.versions).unwrap_or_default();
let versions = version_item::Version::get_many(&version_ids, &**pool, &redis).await?;
for version in versions {
let mut fields = HashMap::new();
fields.insert("client_side".to_string(), json!(client_side));
fields.insert("server_side".to_string(), json!(server_side));
let version = Version::from(version);
let mut fields = version.fields;
let (current_client_side, current_server_side) =
v2_reroute::convert_side_types_v2(&fields);
let client_side = client_side.unwrap_or(current_client_side);
let server_side = server_side.unwrap_or(current_server_side);
fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side));
response = v3::versions::version_edit_helper(
req.clone(),
(version.inner.id.into(),),
(version.id,),
pool.clone(),
redis.clone(),
v3::versions::EditVersion {

View File

@@ -3,10 +3,12 @@ use std::collections::HashMap;
use super::ApiError;
use crate::database::models::loader_fields::LoaderFieldEnumValue;
use crate::database::redis::RedisPool;
use crate::models::v2::projects::LegacySideType;
use crate::routes::v3::tags::{LoaderData as LoaderDataV3, LoaderFieldsEnumQuery};
use crate::routes::{v2_reroute, v3};
use actix_web::{get, web, HttpResponse};
use chrono::{DateTime, Utc};
use itertools::Itertools;
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
@@ -191,28 +193,15 @@ pub async fn project_type_list(
}
#[get("side_type")]
pub async fn side_type_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let response = v3::tags::loader_fields_list(
pool,
web::Query(LoaderFieldsEnumQuery {
loader_field: "client_side".to_string(), // same as server_side
filters: None,
}),
redis,
)
.await?;
// Convert to V2 format
Ok(
match v2_reroute::extract_ok_json::<Vec<LoaderFieldEnumValue>>(response).await {
Ok(fields) => {
let fields = fields.into_iter().map(|f| f.value).collect::<Vec<_>>();
HttpResponse::Ok().json(fields)
}
Err(response) => response,
},
)
pub async fn side_type_list() -> Result<HttpResponse, ApiError> {
// Original side types are no longer reflected in the database.
// Therefore, we hardcode and return all the fields that are supported by our v2 conversion logic.
let side_types = [
LegacySideType::Required,
LegacySideType::Optional,
LegacySideType::Unsupported,
LegacySideType::Unknown,
];
let side_types = side_types.iter().map(|s| s.to_string()).collect_vec();
Ok(HttpResponse::Ok().json(side_types))
}

View File

@@ -1,3 +1,5 @@
use crate::database::models::loader_fields::VersionField;
use crate::database::models::{project_item, version_item};
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::ids::ImageId;
@@ -88,63 +90,90 @@ pub async fn version_create(
payload,
req.headers().clone(),
|legacy_create: InitialVersionData| {
// Convert input data to V3 format
let mut fields = HashMap::new();
fields.insert(
"game_versions".to_string(),
json!(legacy_create.game_versions),
);
let client = client.clone();
let redis = redis.clone();
async move {
// Convert input data to V3 format
let mut fields = HashMap::new();
fields.insert(
"game_versions".to_string(),
json!(legacy_create.game_versions),
);
// TODO: will be overhauled with side-types overhaul
// TODO: if not, should default to previous version
fields.insert("client_side".to_string(), json!("required"));
fields.insert("server_side".to_string(), json!("optional"));
// Handle project type via file extension prediction
let mut project_type = None;
for file_part in &legacy_create.file_parts {
if let Some(ext) = file_part.split('.').last() {
match ext {
"mrpack" | "mrpack-primary" => {
project_type = Some("modpack");
break;
// Copies side types of another version of the project.
// If no version exists, defaults to all false.
// TODO: write test for this to ensure predictible unchanging behaviour
// This is inherently lossy, but not much can be done about it, as side types are no longer associated with projects,
// so the 'missing' ones can't be easily accessed.
let side_type_loader_field_names = [
"singleplayer",
"client_and_server",
"client_only",
"server_only",
];
fields.extend(
side_type_loader_field_names
.iter()
.map(|f| (f.to_string(), json!(false))),
);
if let Some(example_version_fields) =
get_example_version_fields(legacy_create.project_id, client, &redis).await?
{
fields.extend(example_version_fields.into_iter().filter_map(|f| {
if side_type_loader_field_names.contains(&f.field_name.as_str()) {
Some((f.field_name, f.value.serialize_internal()))
} else {
None
}
// No other type matters
_ => {}
}
break;
}));
}
// Handle project type via file extension prediction
let mut project_type = None;
for file_part in &legacy_create.file_parts {
if let Some(ext) = file_part.split('.').last() {
match ext {
"mrpack" | "mrpack-primary" => {
project_type = Some("modpack");
break;
}
// No other type matters
_ => {}
}
break;
}
}
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.
// Setting of 'project_type' directly is removed, it's loader-based now.
if project_type == Some("modpack") {
fields.insert("mrpack_loaders".to_string(), json!(legacy_create.loaders));
}
let loaders = if project_type == Some("modpack") {
vec![Loader("mrpack".to_string())]
} else {
legacy_create.loaders
};
Ok(v3::version_creation::InitialVersionData {
project_id: legacy_create.project_id,
file_parts: legacy_create.file_parts,
version_number: legacy_create.version_number,
version_title: legacy_create.version_title,
version_body: legacy_create.version_body,
dependencies: legacy_create.dependencies,
release_channel: legacy_create.release_channel,
loaders,
featured: legacy_create.featured,
primary_file: legacy_create.primary_file,
status: legacy_create.status,
file_types: legacy_create.file_types,
uploaded_images: legacy_create.uploaded_images,
ordering: legacy_create.ordering,
fields,
})
}
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.
// Setting of 'project_type' directly is removed, it's loader-based now.
if project_type == Some("modpack") {
fields.insert("mrpack_loaders".to_string(), json!(legacy_create.loaders));
}
let loaders = if project_type == Some("modpack") {
vec![Loader("mrpack".to_string())]
} else {
legacy_create.loaders
};
Ok(v3::version_creation::InitialVersionData {
project_id: legacy_create.project_id,
file_parts: legacy_create.file_parts,
version_number: legacy_create.version_number,
version_title: legacy_create.version_title,
version_body: legacy_create.version_body,
dependencies: legacy_create.dependencies,
release_channel: legacy_create.release_channel,
loaders,
featured: legacy_create.featured,
primary_file: legacy_create.primary_file,
status: legacy_create.status,
file_types: legacy_create.file_types,
uploaded_images: legacy_create.uploaded_images,
ordering: legacy_create.ordering,
fields,
})
},
)
.await?;
@@ -170,6 +199,32 @@ pub async fn version_create(
}
}
// Gets version fields of an example version of a project, if one exists.
async fn get_example_version_fields(
project_id: Option<ProjectId>,
pool: Data<PgPool>,
redis: &RedisPool,
) -> Result<Option<Vec<VersionField>>, CreateError> {
let project_id = match project_id {
Some(project_id) => project_id,
None => return Ok(None),
};
let vid = match project_item::Project::get_id(project_id.into(), &**pool, redis)
.await?
.and_then(|p| p.versions.first().cloned())
{
Some(vid) => vid,
None => return Ok(None),
};
let example_version = match version_item::Version::get(vid, &**pool, redis).await? {
Some(version) => version,
None => return Ok(None),
};
Ok(Some(example_version.version_fields))
}
// under /api/v1/version/{version_id}
#[post("{version_id}/file")]
pub async fn upload_file_to_version(

View File

@@ -1,10 +1,14 @@
use std::collections::HashMap;
use super::v3::project_creation::CreateError;
use crate::models::v2::projects::LegacySideType;
use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData};
use actix_multipart::Multipart;
use actix_web::http::header::{HeaderMap, TryIntoHeaderPair};
use actix_web::HttpResponse;
use futures::{stream, StreamExt};
use serde_json::json;
use futures::{stream, Future, StreamExt};
use itertools::Itertools;
use serde_json::{json, Value};
pub async fn extract_ok_json<T>(response: HttpResponse) -> Result<T, HttpResponse>
where
@@ -29,14 +33,15 @@ where
}
}
pub async fn alter_actix_multipart<T, U>(
pub async fn alter_actix_multipart<T, U, Fut>(
mut multipart: Multipart,
mut headers: HeaderMap,
mut closure: impl FnMut(T) -> Result<U, CreateError>,
mut closure: impl FnMut(T) -> Fut,
) -> Result<Multipart, CreateError>
where
T: serde::de::DeserializeOwned,
U: serde::Serialize,
Fut: Future<Output = Result<U, CreateError>>,
{
let mut segments: Vec<MultipartSegment> = Vec::new();
@@ -56,7 +61,7 @@ where
{
let json_value: T = serde_json::from_slice(&buffer)?;
let json_value: U = closure(json_value)?;
let json_value: U = closure(json_value).await?;
buffer = serde_json::to_vec(&json_value)?;
}
@@ -110,3 +115,353 @@ where
Ok(new_multipart)
}
// Converts a "client_side" and "server_side" pair into the new v3 corresponding fields
pub fn convert_side_types_v3(
client_side: LegacySideType,
server_side: LegacySideType,
) -> HashMap<String, Value> {
use LegacySideType::{Optional, Required};
let singleplayer = client_side == Required
|| client_side == Optional
|| server_side == Required
|| server_side == Optional;
let client_and_server = singleplayer;
let client_only =
(client_side == Required || client_side == Optional) && server_side != Required;
let server_only =
(server_side == Required || server_side == Optional) && client_side != Required;
let mut fields = HashMap::new();
fields.insert("singleplayer".to_string(), json!(singleplayer));
fields.insert("client_and_server".to_string(), json!(client_and_server));
fields.insert("client_only".to_string(), json!(client_only));
fields.insert("server_only".to_string(), json!(server_only));
fields
}
// Convert search facets from V2 to V3
// Less trivial as we need to handle the case where one side is set and the other is not, which does not convert cleanly
pub fn convert_side_type_facets_v3(facets: Vec<Vec<Vec<String>>>) -> Vec<Vec<Vec<String>>> {
use LegacySideType::{Optional, Required, Unsupported};
let possible_side_types = [Required, Optional, Unsupported]; // Should not include Unknown
let mut v3_facets = vec![];
// Outer facets are joined by AND
for inner_facets in facets {
// Inner facets are joined by OR
// These may change as the inner facets are converted
// ie:
// for A v B v C, if A is converted to X^Y v Y^Z, then the new facets are X^Y v Y^Z v B v C
let mut new_inner_facets = vec![];
for inner_inner_facets in inner_facets {
// Inner inner facets are joined by AND
let mut client_side = None;
let mut server_side = None;
// Extract client_side and server_side facets, and remove them from the list
let inner_inner_facets = inner_inner_facets
.into_iter()
.filter_map(|facet| {
let val = match facet.split(':').nth(1) {
Some(val) => val,
None => return Some(facet.to_string()),
};
if facet.starts_with("client_side:") {
client_side = Some(LegacySideType::from_string(val));
None
} else if facet.starts_with("server_side:") {
server_side = Some(LegacySideType::from_string(val));
None
} else {
Some(facet.to_string())
}
})
.collect_vec();
// Depending on whether client_side and server_side are set, we can convert the facets to the new loader fields differently
let mut new_possibilities = match (client_side, server_side) {
// Both set or unset is a trivial case
(Some(client_side), Some(server_side)) => {
vec![convert_side_types_v3(client_side, server_side)
.into_iter()
.map(|(k, v)| format!("{}:{}", k, v))
.collect()]
}
(None, None) => vec![vec![]],
(Some(client_side), None) => possible_side_types
.iter()
.map(|server_side| {
convert_side_types_v3(client_side, *server_side)
.into_iter()
.map(|(k, v)| format!("{}:{}", k, v))
.unique()
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
(None, Some(server_side)) => possible_side_types
.iter()
.map(|client_side| {
convert_side_types_v3(*client_side, server_side)
.into_iter()
.map(|(k, v)| format!("{}:{}", k, v))
.unique()
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
};
// Add the new possibilities to the list
for new_possibility in &mut new_possibilities {
new_possibility.extend(inner_inner_facets.clone());
}
new_inner_facets.extend(new_possibilities);
}
v3_facets.push(new_inner_facets);
}
v3_facets
}
// Convert search facets from V3 back to v2
// this is not lossless. (See tests)
pub fn convert_side_types_v2(
side_types: &HashMap<String, Value>,
) -> (LegacySideType, LegacySideType) {
use LegacySideType::{Optional, Required, Unsupported};
let client_and_server = side_types
.get("client_and_server")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let singleplayer = side_types
.get("singleplayer")
.and_then(|x| x.as_bool())
.unwrap_or(client_and_server);
let client_only = side_types
.get("client_only")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let server_only = side_types
.get("server_only")
.and_then(|x| x.as_bool())
.unwrap_or(false);
match (singleplayer, client_only, server_only) {
// Only singleplayer
(true, false, false) => (Required, Required),
// Client only and not server only
(false, true, false) => (Required, Unsupported),
(true, true, false) => (Required, Unsupported),
// Server only and not client only
(false, false, true) => (Unsupported, Required),
(true, false, true) => (Unsupported, Required),
// Both server only and client only
(true, true, true) => (Optional, Optional),
(false, true, true) => (Optional, Optional),
// Bad type
(false, false, false) => (Unsupported, Unsupported),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::v2::projects::LegacySideType::{Optional, Required, Unsupported};
#[test]
fn convert_types() {
// Converting types from V2 to V3 and back should be idempotent- for certain pairs
let lossy_pairs = [
(Optional, Unsupported),
(Unsupported, Optional),
(Required, Optional),
(Optional, Required),
];
for client_side in [Required, Optional, Unsupported] {
for server_side in [Required, Optional, Unsupported] {
if lossy_pairs.contains(&(client_side, server_side)) {
continue;
}
let side_types = convert_side_types_v3(client_side, server_side);
let (client_side2, server_side2) = convert_side_types_v2(&side_types);
assert_eq!(client_side, client_side2);
assert_eq!(server_side, server_side2);
}
}
}
#[test]
fn convert_facets() {
let pre_facets = vec![
// Test combinations of both sides being set
vec![vec![
"client_side:required".to_string(),
"server_side:required".to_string(),
]],
vec![vec![
"client_side:required".to_string(),
"server_side:optional".to_string(),
]],
vec![vec![
"client_side:required".to_string(),
"server_side:unsupported".to_string(),
]],
vec![vec![
"client_side:optional".to_string(),
"server_side:required".to_string(),
]],
vec![vec![
"client_side:optional".to_string(),
"server_side:optional".to_string(),
]],
// Test multiple inner facets
vec![
vec![
"client_side:required".to_string(),
"server_side:required".to_string(),
],
vec![
"client_side:required".to_string(),
"server_side:optional".to_string(),
],
],
// Test additional fields
vec![
vec![
"random_field_test_1".to_string(),
"client_side:required".to_string(),
"server_side:required".to_string(),
],
vec![
"random_field_test_2".to_string(),
"client_side:required".to_string(),
"server_side:optional".to_string(),
],
],
// Test only one facet being set
vec![vec!["client_side:required".to_string()]],
];
let converted_facets = convert_side_type_facets_v3(pre_facets)
.into_iter()
.map(|x| {
x.into_iter()
.map(|mut y| {
y.sort();
y
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let post_facets = vec![
vec![vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:false".to_string(),
"server_only:false".to_string(),
]],
vec![vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:false".to_string(),
]],
vec![vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:false".to_string(),
]],
vec![vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:false".to_string(),
"server_only:true".to_string(),
]],
vec![vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:true".to_string(),
]],
vec![
vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:false".to_string(),
"server_only:false".to_string(),
],
vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:false".to_string(),
],
],
vec![
vec![
"random_field_test_1".to_string(),
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:false".to_string(),
"server_only:false".to_string(),
],
vec![
"random_field_test_2".to_string(),
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:false".to_string(),
],
],
// Test only one facet being set
// Iterates over all possible side types
vec![
// C: Required, S: Required
vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:false".to_string(),
"server_only:false".to_string(),
],
// C: Required, S: Optional
vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:false".to_string(),
],
// C: Required, S: Unsupported
vec![
"singleplayer:true".to_string(),
"client_and_server:true".to_string(),
"client_only:true".to_string(),
"server_only:false".to_string(),
],
],
]
.into_iter()
.map(|x| {
x.into_iter()
.map(|mut y| {
y.sort();
y
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
assert_eq!(converted_facets, post_facets);
}
}

View File

@@ -511,7 +511,6 @@ pub async fn revoke_oauth_authorization(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
println!("Inside revoke_oauth_authorization");
let current_user = get_user_from_headers(
&req,
&**pool,

View File

@@ -8,6 +8,7 @@ use crate::database::models::loader_fields::{
use crate::database::redis::RedisPool;
use actix_web::{web, HttpResponse};
use itertools::Itertools;
use serde_json::Value;
use sqlx::PgPool;
@@ -84,6 +85,7 @@ pub struct LoaderData {
pub name: String,
pub supported_project_types: Vec<String>,
pub supported_games: Vec<String>,
pub supported_fields: Vec<String>, // Available loader fields for this loader
pub metadata: Value,
}
@@ -91,14 +93,26 @@ pub async fn loader_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let mut results = Loader::list(&**pool, &redis)
.await?
let loaders = Loader::list(&**pool, &redis).await?;
let loader_fields = LoaderField::get_fields_per_loader(
&loaders.iter().map(|x| x.id).collect_vec(),
&**pool,
&redis,
)
.await?;
let mut results = loaders
.into_iter()
.map(|x| LoaderData {
icon: x.icon,
name: x.loader,
supported_project_types: x.supported_project_types,
supported_games: x.supported_games,
supported_fields: loader_fields
.get(&x.id)
.map(|x| x.iter().map(|x| x.field.clone()).collect_vec())
.unwrap_or_default(),
metadata: x.metadata,
})
.collect::<Vec<_>>();

View File

@@ -3,8 +3,10 @@ use crate::models::projects::SearchRequest;
use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use chrono::{DateTime, Utc};
use itertools::Itertools;
use meilisearch_sdk::client::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
use std::cmp::min;
use std::collections::HashMap;
@@ -177,7 +179,7 @@ pub async fn search_for_project(
query.with_filter(new_filters);
} else {
let facets = if let Some(facets) = &info.facets {
Some(serde_json::from_str::<Vec<Vec<&str>>>(facets)?)
Some(serde_json::from_str::<Vec<Vec<Value>>>(facets)?)
} else {
None
};
@@ -190,14 +192,42 @@ pub async fn search_for_project(
};
if let Some(facets) = facets {
// Search can now *optionally* have a third inner array: So Vec(AND)<Vec(OR)<Vec(AND)< _ >>>
// For every inner facet, we will check if it can be deserialized into a Vec<&str>, and do so.
// If not, we will assume it is a single facet and wrap it in a Vec.
let facets: Vec<Vec<Vec<String>>> = facets
.into_iter()
.map(|facets| {
facets
.into_iter()
.map(|facet| {
if facet.is_array() {
serde_json::from_value::<Vec<String>>(facet).unwrap_or_default()
} else {
vec![serde_json::from_value::<String>(facet.clone())
.unwrap_or_default()]
}
})
.collect_vec()
})
.collect_vec();
filter_string.push('(');
for (index, facet_list) in facets.iter().enumerate() {
for (index, facet_outer_list) in facets.iter().enumerate() {
filter_string.push('(');
for (facet_index, facet) in facet_list.iter().enumerate() {
filter_string.push_str(&facet.replace(':', " = "));
for (facet_outer_index, facet_inner_list) in facet_outer_list.iter().enumerate()
{
filter_string.push('(');
for (facet_inner_index, facet) in facet_inner_list.iter().enumerate() {
filter_string.push_str(&facet.replace(':', " = "));
if facet_inner_index != (facet_inner_list.len() - 1) {
filter_string.push_str(" AND ")
}
}
filter_string.push(')');
if facet_index != (facet_list.len() - 1) {
if facet_outer_index != (facet_outer_list.len() - 1) {
filter_string.push_str(" OR ")
}
}