Make changelog in version response optional (#5115)

* Make changelog on version routes optional

* fix clippy

* fix ci
This commit is contained in:
aecsocket
2026-01-14 10:55:20 +00:00
committed by GitHub
parent d055dc68dc
commit f85a2d3ec1
16 changed files with 199 additions and 54 deletions

View File

@@ -306,7 +306,8 @@ pub struct LegacyVersion {
pub featured: bool, pub featured: bool,
pub name: String, pub name: String,
pub version_number: String, pub version_number: String,
pub changelog: String, #[serde(skip_serializing_if = "Option::is_none")]
pub changelog: Option<String>,
pub changelog_url: Option<String>, pub changelog_url: Option<String>,
pub date_published: DateTime<Utc>, pub date_published: DateTime<Utc>,
pub downloads: u32, pub downloads: u32,

View File

@@ -661,7 +661,8 @@ pub struct Version {
/// Games for which this version is compatible with, extracted from Loader/Project types /// Games for which this version is compatible with, extracted from Loader/Project types
pub games: Vec<String>, pub games: Vec<String>,
/// The changelog for this version of the project. /// The changelog for this version of the project.
pub changelog: String, #[serde(skip_serializing_if = "Option::is_none")]
pub changelog: Option<String>,
/// The date that this version was published. /// The date that this version was published.
pub date_published: DateTime<Utc>, pub date_published: DateTime<Utc>,
@@ -714,7 +715,7 @@ impl From<VersionQueryResult> for Version {
version_number: v.version_number, version_number: v.version_number,
project_types: data.project_types, project_types: data.project_types,
games: data.games, games: data.games,
changelog: v.changelog, changelog: Some(v.changelog),
date_published: v.date_published, date_published: v.date_published,
downloads: v.downloads as u32, downloads: v.downloads as u32,
version_type: match v.version_type.as_str() { version_type: match v.version_type.as_str() {

View File

@@ -37,6 +37,12 @@ pub struct VersionListFilters {
pub version_type: Option<VersionType>, pub version_type: Option<VersionType>,
pub limit: Option<usize>, pub limit: Option<usize>,
pub offset: Option<usize>, pub offset: Option<usize>,
#[serde(default = "default_true")]
pub include_changelog: bool,
}
fn default_true() -> bool {
true
} }
#[get("version")] #[get("version")]
@@ -95,6 +101,7 @@ pub async fn version_list(
version_type: filters.version_type, version_type: filters.version_type,
limit: filters.limit, limit: filters.limit,
offset: filters.offset, offset: filters.offset,
include_changelog: filters.include_changelog,
}; };
let response = v3::versions::version_list( let response = v3::versions::version_list(
@@ -153,6 +160,8 @@ pub async fn version_project_get(
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct VersionIds { pub struct VersionIds {
pub ids: String, pub ids: String,
#[serde(default = "default_true")]
pub include_changelog: bool,
} }
#[get("versions")] #[get("versions")]
@@ -163,7 +172,10 @@ pub async fn versions_get(
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let ids = v3::versions::VersionIds { ids: ids.ids }; let ids = v3::versions::VersionIds {
ids: ids.ids,
include_changelog: ids.include_changelog,
};
let response = v3::versions::versions_get( let response = v3::versions::versions_get(
req, req,
web::Query(ids), web::Query(ids),

View File

@@ -436,7 +436,7 @@ async fn version_create_inner(
version_number: builder.version_number.clone(), version_number: builder.version_number.clone(),
project_types: all_project_types, project_types: all_project_types,
games: all_games, games: all_games,
changelog: builder.changelog.clone(), changelog: Some(builder.changelog.clone()),
date_published: Utc::now(), date_published: Utc::now(),
downloads: 0, downloads: 0,
version_type: version_data.release_channel, version_type: version_data.release_channel,

View File

@@ -121,6 +121,12 @@ pub async fn version_project_get_helper(
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct VersionIds { pub struct VersionIds {
pub ids: String, pub ids: String,
#[serde(default = "default_true")]
pub include_changelog: bool,
}
fn default_true() -> bool {
true
} }
pub async fn versions_get( pub async fn versions_get(
@@ -150,10 +156,16 @@ pub async fn versions_get(
.map(|x| x.1) .map(|x| x.1)
.ok(); .ok();
let versions = let mut versions =
filter_visible_versions(versions_data, &user_option, &pool, &redis) filter_visible_versions(versions_data, &user_option, &pool, &redis)
.await?; .await?;
if !ids.include_changelog {
for version in &mut versions {
version.changelog = None;
}
}
Ok(HttpResponse::Ok().json(versions)) Ok(HttpResponse::Ok().json(versions))
} }
@@ -715,6 +727,8 @@ pub struct VersionListFilters {
Returns if it matches any of the values Returns if it matches any of the values
*/ */
pub loader_fields: Option<String>, pub loader_fields: Option<String>,
#[serde(default = "default_true")]
pub include_changelog: bool,
} }
pub async fn version_list( pub async fn version_list(
@@ -856,10 +870,16 @@ pub async fn version_list(
}); });
response.dedup_by(|a, b| a.inner.id == b.inner.id); response.dedup_by(|a, b| a.inner.id == b.inner.id);
let response = let mut response =
filter_visible_versions(response, &user_option, &pool, &redis) filter_visible_versions(response, &user_option, &pool, &redis)
.await?; .await?;
if !filters.include_changelog {
for version in &mut response {
version.changelog = None;
}
}
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
} else { } else {
Err(ApiError::NotFound) Err(ApiError::NotFound)

View File

@@ -84,7 +84,7 @@ pub async fn test_patch_version() {
.await; .await;
assert_eq!(version.name, "new version name"); assert_eq!(version.name, "new version name");
assert_eq!(version.version_number, "1.3.0"); assert_eq!(version.version_number, "1.3.0");
assert_eq!(version.changelog, "new changelog"); assert_eq!(version.changelog, Some("new changelog".into()));
assert_eq!( assert_eq!(
version.version_type, version.version_type,
serde_json::from_str::<VersionType>("\"beta\"").unwrap() serde_json::from_str::<VersionType>("\"beta\"").unwrap()

View File

@@ -13,7 +13,9 @@ const _: () = {
use crate::{MuralError, RequestExt}; use crate::{MuralError, RequestExt};
impl crate::Client { impl crate::Client {
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> { pub async fn get_all_accounts(
&self,
) -> Result<Vec<Account>, MuralError> {
maybe_mock!(self, get_all_accounts()); maybe_mock!(self, get_all_accounts());
self.http_get(|base| format!("{base}/api/accounts")) self.http_get(|base| format!("{base}/api/accounts"))
@@ -21,7 +23,10 @@ const _: () = {
.await .await
} }
pub async fn get_account(&self, id: AccountId) -> Result<Account, MuralError> { pub async fn get_account(
&self,
id: AccountId,
) -> Result<Account, MuralError> {
maybe_mock!(self, get_account(id)); maybe_mock!(self, get_account(id));
self.http_get(|base| format!("{base}/api/accounts/{id}")) self.http_get(|base| format!("{base}/api/accounts/{id}"))
@@ -43,7 +48,10 @@ const _: () = {
maybe_mock!( maybe_mock!(
self, self,
create_account(name.as_ref(), description.as_ref().map(AsRef::as_ref)) create_account(
name.as_ref(),
description.as_ref().map(AsRef::as_ref)
)
); );
let body = Body { let body = Body {
@@ -59,7 +67,18 @@ const _: () = {
} }
}; };
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] #[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())] #[display("{}", _0.hyphenated())]
pub struct AccountId(pub Uuid); pub struct AccountId(pub Uuid);

View File

@@ -69,7 +69,8 @@ impl fmt::Display for ApiError {
if !self.params.is_empty() { if !self.params.is_empty() {
lines.push("params:".into()); lines.push("params:".into());
lines.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}"))); lines
.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
} }
lines.push(format!("error name: {}", self.name)); lines.push(format!("error name: {}", self.name));

View File

@@ -2,10 +2,11 @@
use { use {
crate::{ crate::{
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId, CreateCounterparty, Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId,
CreatePayout, FiatAndRailCode, FiatFeeRequest, FiatPayoutFee, MuralError, Organization, CreateCounterparty, CreatePayout, FiatAndRailCode, FiatFeeRequest,
OrganizationId, PayoutMethod, PayoutMethodDetails, PayoutMethodId, PayoutRequest, FiatPayoutFee, MuralError, Organization, OrganizationId, PayoutMethod,
PayoutRequestId, PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse, PayoutMethodDetails, PayoutMethodId, PayoutRequest, PayoutRequestId,
PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
TokenFeeRequest, TokenPayoutFee, UpdateCounterparty, TokenFeeRequest, TokenPayoutFee, UpdateCounterparty,
transaction::{Transaction, TransactionId}, transaction::{Transaction, TransactionId},
}, },

View File

@@ -46,26 +46,40 @@ impl Client {
api_url: String::new(), api_url: String::new(),
api_key: SecretString::from(String::new()), api_key: SecretString::from(String::new()),
transfer_api_key: SecretString::from(String::new()), transfer_api_key: SecretString::from(String::new()),
mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(mock)), mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(
mock,
)),
} }
} }
fn http_req(&self, make_req: impl FnOnce() -> RequestBuilder) -> RequestBuilder { fn http_req(
&self,
make_req: impl FnOnce() -> RequestBuilder,
) -> RequestBuilder {
make_req() make_req()
.bearer_auth(self.api_key.expose_secret()) .bearer_auth(self.api_key.expose_secret())
.header("accept", "application/json") .header("accept", "application/json")
.header("content-type", "application/json") .header("content-type", "application/json")
} }
pub(crate) fn http_get<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { pub(crate) fn http_get<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.get(make_url(&self.api_url))) self.http_req(|| self.http.get(make_url(&self.api_url)))
} }
pub(crate) fn http_post<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { pub(crate) fn http_post<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.post(make_url(&self.api_url))) self.http_req(|| self.http.post(make_url(&self.api_url)))
} }
pub(crate) fn http_put<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { pub(crate) fn http_put<U: IntoUrl>(
&self,
make_url: impl FnOnce(&str) -> U,
) -> RequestBuilder {
self.http_req(|| self.http.put(make_url(&self.api_url))) self.http_req(|| self.http.put(make_url(&self.api_url)))
} }

View File

@@ -15,7 +15,8 @@ const _: () = {
pub async fn search_counterparties( pub async fn search_counterparties(
&self, &self,
params: Option<SearchParams<CounterpartyId>>, params: Option<SearchParams<CounterpartyId>>,
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> { ) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError>
{
maybe_mock!(self, search_counterparties(params)); maybe_mock!(self, search_counterparties(params));
self.http_post(|base| format!("{base}/api/counterparties/search")) self.http_post(|base| format!("{base}/api/counterparties/search"))
@@ -30,9 +31,11 @@ const _: () = {
) -> Result<Counterparty, MuralError> { ) -> Result<Counterparty, MuralError> {
maybe_mock!(self, get_counterparty(id)); maybe_mock!(self, get_counterparty(id));
self.http_get(|base| format!("{base}/api/counterparties/counterparty/{id}")) self.http_get(|base| {
.send_mural() format!("{base}/api/counterparties/counterparty/{id}")
.await })
.send_mural()
.await
} }
pub async fn create_counterparty( pub async fn create_counterparty(
@@ -70,15 +73,28 @@ const _: () = {
let body = Body { counterparty }; let body = Body { counterparty };
self.http_put(|base| format!("{base}/api/counterparties/counterparty/{id}")) self.http_put(|base| {
.json(&body) format!("{base}/api/counterparties/counterparty/{id}")
.send_mural() })
.await .json(&body)
.send_mural()
.await
} }
} }
}; };
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] #[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())] #[display("{}", _0.hyphenated())]
pub struct CounterpartyId(pub Uuid); pub struct CounterpartyId(pub Uuid);

View File

@@ -15,7 +15,8 @@ const _: () = {
pub async fn search_organizations( pub async fn search_organizations(
&self, &self,
req: SearchRequest, req: SearchRequest,
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> { ) -> Result<SearchResponse<OrganizationId, Organization>, MuralError>
{
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Body { struct Body {
@@ -41,8 +42,9 @@ const _: () = {
let query = [ let query = [
req.limit.map(|limit| ("limit", limit.to_string())), req.limit.map(|limit| ("limit", limit.to_string())),
req.next_id req.next_id.map(|next_id| {
.map(|next_id| ("nextId", next_id.hyphenated().to_string())), ("nextId", next_id.hyphenated().to_string())
}),
] ]
.into_iter() .into_iter()
.flatten() .flatten()
@@ -75,7 +77,18 @@ const _: () = {
} }
}; };
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] #[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())] #[display("{}", _0.hyphenated())]
pub struct OrganizationId(pub Uuid); pub struct OrganizationId(pub Uuid);

View File

@@ -1,8 +1,8 @@
use { use {
crate::{ crate::{
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId, CrcSymbol, ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId,
DocumentType, EurSymbol, FiatAccountType, MxnSymbol, PenSymbol, UsdSymbol, WalletDetails, CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MxnSymbol,
ZarSymbol, PenSymbol, UsdSymbol, WalletDetails, ZarSymbol,
}, },
chrono::{DateTime, Utc}, chrono::{DateTime, Utc},
derive_more::{Deref, Display, Error}, derive_more::{Deref, Display, Error},
@@ -21,7 +21,8 @@ const _: () = {
&self, &self,
counterparty_id: CounterpartyId, counterparty_id: CounterpartyId,
params: Option<SearchParams<PayoutMethodId>>, params: Option<SearchParams<PayoutMethodId>>,
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> { ) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError>
{
maybe_mock!(self, search_payout_methods(counterparty_id, params)); maybe_mock!(self, search_payout_methods(counterparty_id, params));
self.http_post(|base| { self.http_post(|base| {
@@ -37,7 +38,10 @@ const _: () = {
counterparty_id: CounterpartyId, counterparty_id: CounterpartyId,
payout_method_id: PayoutMethodId, payout_method_id: PayoutMethodId,
) -> Result<PayoutMethod, MuralError> { ) -> Result<PayoutMethod, MuralError> {
maybe_mock!(self, get_payout_method(counterparty_id, payout_method_id)); maybe_mock!(
self,
get_payout_method(counterparty_id, payout_method_id)
);
self.http_get(|base| { self.http_get(|base| {
format!( format!(
@@ -63,7 +67,11 @@ const _: () = {
maybe_mock!( maybe_mock!(
self, self,
create_payout_method(counterparty_id, alias.as_ref(), payout_method) create_payout_method(
counterparty_id,
alias.as_ref(),
payout_method
)
); );
let body = Body { let body = Body {
@@ -72,7 +80,9 @@ const _: () = {
}; };
self.http_post(|base| { self.http_post(|base| {
format!("{base}/api/counterparties/{counterparty_id}/payout-methods") format!(
"{base}/api/counterparties/{counterparty_id}/payout-methods"
)
}) })
.json(&body) .json(&body)
.send_mural() .send_mural()
@@ -121,7 +131,18 @@ pub enum PayoutMethodPixAccountType {
BankAccount, BankAccount,
} }
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] #[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())] #[display("{}", _0.hyphenated())]
pub struct PayoutMethodId(pub Uuid); pub struct PayoutMethodId(pub Uuid);

View File

@@ -4,7 +4,10 @@ use {
std::borrow::Cow, std::borrow::Cow,
}; };
pub fn serialize<S: serde::Serializer>(v: &CountryCode, serializer: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: serde::Serializer>(
v: &CountryCode,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(v.alpha2) serializer.serialize_str(v.alpha2)
} }
@@ -15,6 +18,8 @@ pub fn deserialize<'de, D: serde::Deserializer<'de>>(
rust_iso3166::ALPHA2_MAP rust_iso3166::ALPHA2_MAP
.get(&country_code) .get(&country_code)
.copied() .copied()
.ok_or_else(|| D::Error::custom("invalid ISO 3166 alpha-2 country code")) .ok_or_else(|| {
D::Error::custom("invalid ISO 3166 alpha-2 country code")
})
}) })
} }

View File

@@ -5,14 +5,21 @@ use derive_more::{Deref, Display};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount}; use crate::{
AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount,
};
#[cfg(feature = "client")] #[cfg(feature = "client")]
const _: () = { const _: () = {
use crate::{Account, MuralError, RequestExt, SearchParams, SearchResponse}; use crate::{
Account, MuralError, RequestExt, SearchParams, SearchResponse,
};
impl crate::Client { impl crate::Client {
pub async fn get_transaction(&self, id: TransactionId) -> Result<Transaction, MuralError> { pub async fn get_transaction(
&self,
id: TransactionId,
) -> Result<Transaction, MuralError> {
maybe_mock!(self, get_transaction(id)); maybe_mock!(self, get_transaction(id));
self.http_get(|base| format!("{base}/api/transactions/{id}")) self.http_get(|base| format!("{base}/api/transactions/{id}"))
@@ -27,15 +34,28 @@ const _: () = {
) -> Result<SearchResponse<AccountId, Account>, MuralError> { ) -> Result<SearchResponse<AccountId, Account>, MuralError> {
maybe_mock!(self, search_transactions(account_id, params)); maybe_mock!(self, search_transactions(account_id, params));
self.http_post(|base| format!("{base}/api/transactions/search/account/{account_id}")) self.http_post(|base| {
.query(&params.map(|p| p.to_query()).unwrap_or_default()) format!("{base}/api/transactions/search/account/{account_id}")
.send_mural() })
.await .query(&params.map(|p| p.to_query()).unwrap_or_default())
.send_mural()
.await
} }
} }
}; };
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] #[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Deref,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[display("{}", _0.hyphenated())] #[display("{}", _0.hyphenated())]
pub struct TransactionId(pub Uuid); pub struct TransactionId(pub Uuid);

View File

@@ -5,7 +5,8 @@ macro_rules! display_as_serialize {
impl fmt::Display for $T { impl fmt::Display for $T {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = serde_json::to_value(self).map_err(|_| fmt::Error)?; let value =
serde_json::to_value(self).map_err(|_| fmt::Error)?;
let value = value.as_str().ok_or(fmt::Error)?; let value = value.as_str().ok_or(fmt::Error)?;
write!(f, "{value}") write!(f, "{value}")
} }