Compare commits

...
16 changed files with 402 additions and 184 deletions
+8 -45
View File
@@ -1059,15 +1059,11 @@ const currentGameVersion = computed(() => {
})
const possibleGameVersions = computed(() => {
return versions.value
.filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value))
.flatMap((x) => x.game_versions)
return versionsV3.value?.available_game_versions || []
})
const possiblePlatforms = computed(() => {
return versions.value
.filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value))
.flatMap((x) => x.loaders)
return versionsV3.value?.available_loaders || []
})
const currentPlatform = computed(() => {
@@ -1414,29 +1410,11 @@ const filteredVersions = computed(() => {
)
})
const filteredRelease = computed(() => {
return filteredVersions.value.find((x) => x.version_type === 'release')
})
const filteredRelease = computed(() => versionsV3.value?.latest_versions?.release || null)
const filteredBeta = computed(() => {
return filteredVersions.value.find(
(x) =>
x.version_type === 'beta' &&
(!filteredRelease.value ||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))),
)
})
const filteredBeta = computed(() => versionsV3.value?.latest_versions?.beta || null)
const filteredAlpha = computed(() => {
return filteredVersions.value.find(
(x) =>
x.version_type === 'alpha' &&
(!filteredRelease.value ||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))) &&
(!filteredBeta.value ||
dayjs(x.date_published).isAfter(dayjs(filteredBeta.value.date_published))),
)
})
const filteredAlpha = computed(() => versionsV3.value?.latest_versions?.alpha || null)
const displayCollectionsSearch = ref('')
const collections = computed(() =>
@@ -1472,14 +1450,12 @@ let project,
dependencies,
versions,
versionsV3,
resetVersionsV2,
organization,
resetOrganization,
projectV2Error,
projectV3Error,
membersError,
dependenciesError,
versionsError,
versionsV3Error,
resetVersionsV3
try {
@@ -1488,7 +1464,6 @@ try {
{ data: projectV3, error: projectV3Error, refresh: resetProjectV3 },
{ data: allMembers, error: membersError, refresh: resetMembers },
{ data: dependencies, error: dependenciesError },
{ data: versions, error: versionsError, refresh: resetVersionsV2 },
{ data: versionsV3, error: versionsV3Error, refresh: resetVersionsV3 },
{ data: organization, refresh: resetOrganization },
] = await Promise.all([
@@ -1528,9 +1503,6 @@ try {
useAsyncData(`project/${projectId.value}/dependencies`, () =>
useBaseFetch(`project/${projectId.value}/dependencies`, {}),
),
useAsyncData(`project/${projectId.value}/version`, () =>
useBaseFetch(`project/${projectId.value}/version`),
),
useAsyncData(`project/${projectId.value}/version/v3`, () =>
useBaseFetch(`project/${projectId.value}/version`, { apiVersion: 3 }),
),
@@ -1541,12 +1513,8 @@ try {
await updateProjectRoute()
versions = shallowRef(toRaw(versions))
versionsV3 = shallowRef(toRaw(versionsV3))
versions.value = (versions.value ?? []).map((v) => ({
...v,
environment: versionsV3.value?.find((v3) => v3.id === v.id)?.environment,
}))
versions = shallowRef(toRaw(versionsV3.value?.versions))
versionsV3 = shallowRef(toRaw(versionsV3.value))
} catch (err) {
throw createError({
fatal: true,
@@ -1584,13 +1552,9 @@ async function resetProject() {
}
async function resetVersions() {
await resetVersionsV2()
await resetVersionsV3()
versions.value = (versions.value ?? []).map((v) => ({
...v,
environment: versionsV3.value?.find((v3) => v3.id === v.id)?.environment,
}))
versions.value = versionsV3.value?.versions || []
}
function handleError(err, project = false) {
@@ -1610,7 +1574,6 @@ handleError(projectV2Error, true)
handleError(projectV3Error)
handleError(membersError)
handleError(dependenciesError)
handleError(versionsError)
handleError(versionsV3Error)
if (!project.value) {
+3 -3
View File
@@ -327,7 +327,7 @@ pub async fn run(
.send()
.await
.and_then(|res| res.error_for_status())
.map_err(ApiError::Delphi)?;
.map_err(ApiError::delphi)?;
Ok(HttpResponse::NoContent().finish())
}
@@ -411,10 +411,10 @@ async fn issue_type_schema(
.send()
.await
.and_then(|res| res.error_for_status())
.map_err(ApiError::Delphi)?
.map_err(ApiError::delphi)?
.json::<serde_json::Map<String, serde_json::Value>>()
.await
.map_err(ApiError::Delphi)?,
.map_err(ApiError::delphi)?,
Instant::now(),
))
.0,
+6 -2
View File
@@ -163,8 +163,8 @@ pub enum ApiError {
RateLimitError(u128, u32),
#[error("Error while interacting with payment processor: {0}")]
Stripe(#[from] stripe::StripeError),
#[error("Error while interacting with Delphi: {0}")]
Delphi(reqwest::Error),
#[error("Error while interacting with Delphi: {0:?}")]
Delphi(eyre::Error),
#[error(transparent)]
Mural(#[from] Box<muralpay::ApiError>),
#[error("report still has {} issue details with no verdict", details.len())]
@@ -174,6 +174,10 @@ pub enum ApiError {
}
impl ApiError {
pub fn delphi(err: impl Into<eyre::Error>) -> Self {
Self::Delphi(err.into())
}
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
crate::models::error::ApiError {
error: match self {
+14 -13
View File
@@ -94,10 +94,10 @@ pub async fn version_list(
featured: filters.featured,
version_type: filters.version_type,
limit: filters.limit,
offset: filters.offset,
..Default::default()
};
let response = v3::versions::version_list(
let response = match v3::versions::version_list(
req,
info,
web::Query(filters),
@@ -106,19 +106,20 @@ pub async fn version_list(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)?;
{
Ok(r) => r,
Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")),
Err(e) => return Err(e),
};
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Version>>(response).await {
Ok(versions) => {
let v2_versions = versions
.into_iter()
.map(LegacyVersion::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(v2_versions))
}
Err(response) => Ok(response),
}
let v2_versions = response
.0
.versions
.into_iter()
.map(LegacyVersion::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(v2_versions))
}
// Given a project ID/slug and a version slug
+213 -75
View File
@@ -19,7 +19,7 @@ use crate::models::ids::VersionId;
use crate::models::images::ImageContext;
use crate::models::pats::Scopes;
use crate::models::projects::{
Dependency, FileType, VersionStatus, VersionType,
Dependency, FileType, Version, VersionStatus, VersionType,
};
use crate::models::projects::{Loader, skip_nulls};
use crate::models::teams::ProjectPermissions;
@@ -701,13 +701,13 @@ pub async fn version_edit_helper(
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Validate, Default, Debug)]
pub struct VersionListFilters {
pub loaders: Option<String>,
pub featured: Option<bool>,
pub version_type: Option<VersionType>,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub next_id: Option<VersionId>,
/*
Loader fields to filter with:
"game_versions": ["1.16.5", "1.17"]
@@ -717,6 +717,36 @@ pub struct VersionListFilters {
pub loader_fields: Option<String>,
}
#[derive(Serialize)]
pub struct VersionListResponse {
pub versions: Vec<Version>,
pub available_game_versions: Vec<String>,
pub available_loaders: Vec<Loader>,
pub latest_versions: LatestVersions,
}
#[derive(Serialize)]
pub struct LatestVersions {
pub release: Option<Version>,
pub beta: Option<Version>,
pub alpha: Option<Version>,
}
fn find_latest_by_type(
versions: &[database::models::version_item::VersionQueryResult],
version_type: VersionType,
after_date: Option<chrono::DateTime<chrono::Utc>>,
) -> Option<database::models::version_item::VersionQueryResult> {
versions
.iter()
.filter(|v| {
v.inner.version_type.as_str() == version_type.as_str()
&& after_date.map_or(true, |d| v.inner.date_published > d)
})
.max_by_key(|v| v.inner.date_published)
.cloned()
}
pub async fn version_list(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -724,7 +754,7 @@ pub async fn version_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
) -> Result<web::Json<VersionListResponse>, ApiError> {
let string = info.into_inner().0;
let result =
@@ -748,6 +778,37 @@ pub async fn version_list(
return Err(ApiError::NotFound);
}
// Fetch all versions first (for computing metadata)
let all_versions = database::models::DBVersion::get_many(
&project.versions,
&**pool,
&redis,
)
.await?;
// Compute available_game_versions and available_loaders from ALL versions
let available_game_versions: Vec<String> = all_versions
.iter()
.flat_map(|v| {
v.version_fields
.iter()
.find(|vf| vf.field_name == "game_versions")
.map(|vf| vf.value.as_strings())
.unwrap_or_default()
})
.sorted()
.dedup()
.collect();
let available_loaders: Vec<Loader> = all_versions
.iter()
.flat_map(|v| v.loaders.iter().cloned())
.sorted()
.dedup()
.map(Loader)
.collect();
// Parse filter parameters
let loader_field_filters = filters.loader_fields.as_ref().map(|x| {
serde_json::from_str::<HashMap<String, Vec<serde_json::Value>>>(x)
.unwrap_or_default()
@@ -755,70 +816,100 @@ pub async fn version_list(
let loader_filters = filters.loaders.as_ref().map(|x| {
serde_json::from_str::<Vec<String>>(x).unwrap_or_default()
});
let mut versions = database::models::DBVersion::get_many(
&project.versions,
&**pool,
&redis,
)
.await?
.into_iter()
.skip(filters.offset.unwrap_or(0))
.take(filters.limit.unwrap_or(usize::MAX))
.filter(|x| {
let mut bool = true;
if let Some(version_type) = filters.version_type {
bool &= &*x.inner.version_type == version_type.as_str();
}
if let Some(loaders) = &loader_filters {
bool &= x.loaders.iter().any(|y| loaders.contains(y));
}
if let Some(loader_fields) = &loader_field_filters {
for (key, values) in loader_fields {
bool &= if let Some(x_vf) =
x.version_fields.iter().find(|y| y.field_name == *key)
{
values.iter().any(|v| x_vf.value.contains_json_value(v))
} else {
true
};
// Apply filters to all versions
let mut filtered_versions = all_versions
.into_iter()
.filter(|x| {
let mut valid = true;
if let Some(version_type) = filters.version_type {
valid &= &*x.inner.version_type == version_type.as_str();
}
if let Some(loaders) = &loader_filters {
valid &= x.loaders.iter().any(|y| loaders.contains(y));
}
if let Some(loader_fields) = &loader_field_filters {
for (key, values) in loader_fields {
valid &= if let Some(x_vf) = x
.version_fields
.iter()
.find(|y| y.field_name == *key)
{
values
.iter()
.any(|v| x_vf.value.contains_json_value(v))
} else {
true
};
}
}
if let Some(featured) = filters.featured {
valid &= featured == x.inner.featured;
}
}
bool
})
.collect::<Vec<_>>();
let mut response = versions
.iter()
.filter(|version| {
filters
.featured
.is_none_or(|featured| featured == version.inner.featured)
valid
})
.cloned()
.collect::<Vec<_>>();
versions.sort_by(|a, b| {
b.inner.date_published.cmp(&a.inner.date_published)
// Sort by (date_published DESC, id DESC)
filtered_versions.sort_by(|a, b| {
let date_cmp = b.inner.date_published.cmp(&a.inner.date_published);
if date_cmp == std::cmp::Ordering::Equal {
b.inner.id.0.cmp(&a.inner.id.0)
} else {
date_cmp
}
});
// Clone filtered_versions for later use (auto-featured logic and latest_versions computation)
let filtered_versions_for_pagination = filtered_versions.clone();
// Apply cursor pagination using next_id
let paginated_versions: Vec<_> = match filters.next_id {
Some(next_id) => {
let cursor_index = filtered_versions_for_pagination
.iter()
.position(|v| v.inner.id == next_id.into());
match cursor_index {
Some(idx) => filtered_versions_for_pagination
.into_iter()
.skip(idx + 1)
.take(filters.limit.unwrap_or(20))
.collect(),
None => {
// Invalid cursor, return first page
filtered_versions_for_pagination
.into_iter()
.take(filters.limit.unwrap_or(20))
.collect()
}
}
}
None => filtered_versions_for_pagination
.into_iter()
.take(filters.limit.unwrap_or(20))
.collect(),
};
// Attempt to populate versions with "auto featured" versions
if response.is_empty()
&& !versions.is_empty()
let mut response = if paginated_versions.is_empty()
&& !filtered_versions.is_empty()
&& filters.featured.unwrap_or(false)
{
// TODO: This is a bandaid fix for detecting auto-featured versions.
// In the future, not all versions will have 'game_versions' fields, so this will need to be changed.
let (loaders, game_versions) = futures::future::try_join(
database::models::loader_fields::Loader::list(&**pool, &redis),
database::models::legacy_loader_fields::MinecraftGameVersion::list(
None,
Some(true),
&**pool,
&redis,
),
)
.await?;
database::models::loader_fields::Loader::list(&**pool, &redis),
database::models::legacy_loader_fields::MinecraftGameVersion::list(
None,
Some(true),
&**pool,
&redis,
),
)
.await?;
let mut joined_filters = Vec::new();
for game_version in &game_versions {
@@ -827,40 +918,87 @@ pub async fn version_list(
}
}
let mut auto_featured = Vec::new();
joined_filters.into_iter().for_each(|filter| {
if let Some(version) = versions.iter().find(|version| {
// TODO: This is the bandaid fix for detecting auto-featured versions.
let game_versions = version
.version_fields
.iter()
.find(|vf| vf.field_name == "game_versions")
.map(|vf| vf.value.clone())
.map(|v| v.as_strings())
.unwrap_or_default();
game_versions.contains(&filter.0.version)
&& version.loaders.contains(&filter.1.loader)
}) {
response.push(version.clone());
if let Some(version) =
filtered_versions.iter().find(|version| {
// TODO: This is the bandaid fix for detecting auto-featured versions.
let game_versions = version
.version_fields
.iter()
.find(|vf| vf.field_name == "game_versions")
.map(|vf| vf.value.clone())
.map(|v| v.as_strings())
.unwrap_or_default();
game_versions.contains(&filter.0.version)
&& version.loaders.contains(&filter.1.loader)
})
{
auto_featured.push(version.clone());
}
});
if response.is_empty() {
versions
.into_iter()
.for_each(|version| response.push(version));
if auto_featured.is_empty() {
filtered_versions.clone()
} else {
auto_featured
}
}
} else {
paginated_versions
};
response.sort_by(|a, b| {
b.inner.date_published.cmp(&a.inner.date_published)
let date_cmp = b.inner.date_published.cmp(&a.inner.date_published);
if date_cmp == std::cmp::Ordering::Equal {
b.inner.id.0.cmp(&a.inner.id.0)
} else {
date_cmp
}
});
response.dedup_by(|a, b| a.inner.id == b.inner.id);
let response =
// Compute latest_versions from filtered versions (before pagination)
let filtered_for_latest = filtered_versions.clone();
let latest_release = find_latest_by_type(
&filtered_for_latest,
VersionType::Release,
None,
);
let latest_beta = find_latest_by_type(
&filtered_for_latest,
VersionType::Beta,
latest_release.as_ref().map(|r| r.inner.date_published),
);
let latest_alpha = find_latest_by_type(
&filtered_for_latest,
VersionType::Alpha,
latest_beta
.as_ref()
.or(latest_release.as_ref())
.map(|v| v.inner.date_published),
);
// Filter visible versions
let visible_versions =
filter_visible_versions(response, &user_option, &pool, &redis)
.await?;
Ok(HttpResponse::Ok().json(response))
// Convert to API models
let versions: Vec<Version> =
visible_versions.into_iter().map(Version::from).collect();
let latest_versions = LatestVersions {
release: latest_release.map(Version::from),
beta: latest_beta.map(Version::from),
alpha: latest_alpha.map(Version::from),
};
Ok(web::Json(VersionListResponse {
versions,
available_game_versions,
available_loaders,
latest_versions,
}))
} else {
Err(ApiError::NotFound)
}
+1
View File
@@ -12,6 +12,7 @@ services:
POSTGRES_USER: labrinth
POSTGRES_PASSWORD: labrinth
POSTGRES_HOST_AUTH_METHOD: trust
PGUSER: labrinth
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'labrinth']
interval: 3s
+23 -4
View File
@@ -13,7 +13,9 @@ const _: () = {
use crate::{MuralError, RequestExt};
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());
self.http_get(|base| format!("{base}/api/accounts"))
@@ -21,7 +23,10 @@ const _: () = {
.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));
self.http_get(|base| format!("{base}/api/accounts/{id}"))
@@ -43,7 +48,10 @@ const _: () = {
maybe_mock!(
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 {
@@ -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))]
#[display("{}", _0.hyphenated())]
pub struct AccountId(pub Uuid);
+2 -1
View File
@@ -69,7 +69,8 @@ impl fmt::Display for ApiError {
if !self.params.is_empty() {
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));
+5 -4
View File
@@ -2,10 +2,11 @@
use {
crate::{
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId, CreateCounterparty,
CreatePayout, FiatAndRailCode, FiatFeeRequest, FiatPayoutFee, MuralError, Organization,
OrganizationId, PayoutMethod, PayoutMethodDetails, PayoutMethodId, PayoutRequest,
PayoutRequestId, PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId,
CreateCounterparty, CreatePayout, FiatAndRailCode, FiatFeeRequest,
FiatPayoutFee, MuralError, Organization, OrganizationId, PayoutMethod,
PayoutMethodDetails, PayoutMethodId, PayoutRequest, PayoutRequestId,
PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
TokenFeeRequest, TokenPayoutFee, UpdateCounterparty,
transaction::{Transaction, TransactionId},
},
+19 -5
View File
@@ -46,26 +46,40 @@ impl Client {
api_url: String::new(),
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()
.bearer_auth(self.api_key.expose_secret())
.header("accept", "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)))
}
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)))
}
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)))
}
+25 -9
View File
@@ -15,7 +15,8 @@ const _: () = {
pub async fn search_counterparties(
&self,
params: Option<SearchParams<CounterpartyId>>,
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError>
{
maybe_mock!(self, search_counterparties(params));
self.http_post(|base| format!("{base}/api/counterparties/search"))
@@ -30,9 +31,11 @@ const _: () = {
) -> Result<Counterparty, MuralError> {
maybe_mock!(self, get_counterparty(id));
self.http_get(|base| format!("{base}/api/counterparties/counterparty/{id}"))
.send_mural()
.await
self.http_get(|base| {
format!("{base}/api/counterparties/counterparty/{id}")
})
.send_mural()
.await
}
pub async fn create_counterparty(
@@ -70,15 +73,28 @@ const _: () = {
let body = Body { counterparty };
self.http_put(|base| format!("{base}/api/counterparties/counterparty/{id}"))
.json(&body)
.send_mural()
.await
self.http_put(|base| {
format!("{base}/api/counterparties/counterparty/{id}")
})
.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))]
#[display("{}", _0.hyphenated())]
pub struct CounterpartyId(pub Uuid);
+17 -4
View File
@@ -15,7 +15,8 @@ const _: () = {
pub async fn search_organizations(
&self,
req: SearchRequest,
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> {
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError>
{
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
@@ -41,8 +42,9 @@ const _: () = {
let query = [
req.limit.map(|limit| ("limit", limit.to_string())),
req.next_id
.map(|next_id| ("nextId", next_id.hyphenated().to_string())),
req.next_id.map(|next_id| {
("nextId", next_id.hyphenated().to_string())
}),
]
.into_iter()
.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))]
#[display("{}", _0.hyphenated())]
pub struct OrganizationId(pub Uuid);
+29 -8
View File
@@ -1,8 +1,8 @@
use {
crate::{
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId, CrcSymbol,
DocumentType, EurSymbol, FiatAccountType, MxnSymbol, PenSymbol, UsdSymbol, WalletDetails,
ZarSymbol,
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId,
CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MxnSymbol,
PenSymbol, UsdSymbol, WalletDetails, ZarSymbol,
},
chrono::{DateTime, Utc},
derive_more::{Deref, Display, Error},
@@ -21,7 +21,8 @@ const _: () = {
&self,
counterparty_id: CounterpartyId,
params: Option<SearchParams<PayoutMethodId>>,
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError>
{
maybe_mock!(self, search_payout_methods(counterparty_id, params));
self.http_post(|base| {
@@ -37,7 +38,10 @@ const _: () = {
counterparty_id: CounterpartyId,
payout_method_id: PayoutMethodId,
) -> 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| {
format!(
@@ -63,7 +67,11 @@ const _: () = {
maybe_mock!(
self,
create_payout_method(counterparty_id, alias.as_ref(), payout_method)
create_payout_method(
counterparty_id,
alias.as_ref(),
payout_method
)
);
let body = Body {
@@ -72,7 +80,9 @@ const _: () = {
};
self.http_post(|base| {
format!("{base}/api/counterparties/{counterparty_id}/payout-methods")
format!(
"{base}/api/counterparties/{counterparty_id}/payout-methods"
)
})
.json(&body)
.send_mural()
@@ -121,7 +131,18 @@ pub enum PayoutMethodPixAccountType {
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))]
#[display("{}", _0.hyphenated())]
pub struct PayoutMethodId(pub Uuid);
+7 -2
View File
@@ -4,7 +4,10 @@ use {
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)
}
@@ -15,6 +18,8 @@ pub fn deserialize<'de, D: serde::Deserializer<'de>>(
rust_iso3166::ALPHA2_MAP
.get(&country_code)
.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")
})
})
}
+28 -8
View File
@@ -5,14 +5,21 @@ use derive_more::{Deref, Display};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount};
use crate::{
AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount,
};
#[cfg(feature = "client")]
const _: () = {
use crate::{Account, MuralError, RequestExt, SearchParams, SearchResponse};
use crate::{
Account, MuralError, RequestExt, SearchParams, SearchResponse,
};
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));
self.http_get(|base| format!("{base}/api/transactions/{id}"))
@@ -27,15 +34,28 @@ const _: () = {
) -> Result<SearchResponse<AccountId, Account>, MuralError> {
maybe_mock!(self, search_transactions(account_id, params));
self.http_post(|base| format!("{base}/api/transactions/search/account/{account_id}"))
.query(&params.map(|p| p.to_query()).unwrap_or_default())
.send_mural()
.await
self.http_post(|base| {
format!("{base}/api/transactions/search/account/{account_id}")
})
.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))]
#[display("{}", _0.hyphenated())]
pub struct TransactionId(pub Uuid);
+2 -1
View File
@@ -5,7 +5,8 @@ macro_rules! display_as_serialize {
impl fmt::Display for $T {
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)?;
write!(f, "{value}")
}